├── .gitignore ├── pnrp ├── __main__.py └── __init__.py ├── .style.yapf ├── tests ├── test_runstate.py ├── test_ast_compare.py ├── test_driver.py └── test_ast_diff.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | run.py 3 | -------------------------------------------------------------------------------- /pnrp/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pnrp 3 | 4 | if __name__ == '__main__': 5 | [_, fpath, *args] = sys.argv 6 | pnrp.cli(fpath, args) 7 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | 4 | # max characters per line 5 | COLUMN_LIMIT = 100 6 | 7 | # Put closing brackets on a separate line, dedented, if the bracketed expression can't fit in a single line 8 | DEDENT_CLOSING_BRACKETS = true 9 | 10 | # Place each dictionary entry onto its own line. 11 | EACH_DICT_ENTRY_ON_SEPARATE_LINE = true 12 | 13 | # Join short lines into one line. E.g., single line if statements. 14 | JOIN_MULTIPLE_LINES = true 15 | 16 | # Insert a blank line before a def or class immediately nested within another def or class 17 | BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = true 18 | 19 | # Split before arguments if the argument list is terminated by a comma. 20 | SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED = true 21 | 22 | # If an argument / parameter list is going to be split, then split before the first argument 23 | SPLIT_BEFORE_FIRST_ARGUMENT = true 24 | -------------------------------------------------------------------------------- /tests/test_runstate.py: -------------------------------------------------------------------------------- 1 | import pnrp 2 | import inspect 3 | 4 | 5 | def test_runstate_simple(): 6 | gvars = {} 7 | states = [] 8 | fchanges = [ 9 | """ 10 | a = 'hello' 11 | """, """ 12 | a = 'hello' 13 | print(a) 14 | a *= 2 15 | """ 16 | ] 17 | 18 | fchanges = [('run.py', inspect.cleandoc(x)) for x in fchanges] 19 | pnrp.driver(fchanges, gvars=gvars, flush_runstate=lambda xs: states.append('\n'.join(xs))) 20 | 21 | # Expected States represents too loops through 22 | # change, run, complete 23 | # change, run, complete 24 | expected_states = inspect.cleandoc( 25 | """ 26 | ~ 27 | --- 28 | >> 29 | --- 30 | . 31 | --- 32 | ~ 33 | ~ 34 | ~ 35 | --- 36 | >> 37 | ~ 38 | ~ 39 | --- 40 | ~ 41 | >> 42 | ~ 43 | --- 44 | ~ 45 | ~ 46 | >> 47 | --- 48 | . 49 | . 50 | . 51 | """ 52 | ).split('---') 53 | expected_states = [inspect.cleandoc(x) for x in expected_states] 54 | 55 | assert gvars['a'] == 'hellohello' 56 | assert states == expected_states 57 | -------------------------------------------------------------------------------- /tests/test_ast_compare.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import inspect 3 | import pnrp 4 | 5 | 6 | def str2ast(x): 7 | return ast.parse(inspect.cleandoc(x)) 8 | 9 | 10 | def test_ast_compare_same(): 11 | a = str2ast(""" 12 | a = 1 13 | """) 14 | b = str2ast(""" 15 | a = 1 16 | """) 17 | 18 | assert pnrp.compare_ast(a, b) == True 19 | 20 | 21 | def test_ast_compare_same_recursive(): 22 | a = str2ast(""" 23 | def foo(): 24 | a = 1 25 | """) 26 | b = str2ast(""" 27 | def foo(): 28 | a = 1 29 | """) 30 | 31 | assert pnrp.compare_ast(a, b) == True 32 | 33 | 34 | def test_ast_compare_diff(): 35 | a = str2ast(""" 36 | a = 1 37 | """) 38 | b = str2ast(""" 39 | b = 1 40 | """) 41 | 42 | assert pnrp.compare_ast(a, b) == False 43 | 44 | 45 | def test_ast_compare_diff_top_level(): 46 | a = str2ast(""" 47 | def foo(): 48 | a = 1 49 | """) 50 | b = str2ast(""" 51 | def bar(): 52 | a = 1 53 | """) 54 | 55 | assert pnrp.compare_ast(a, b) == False 56 | 57 | 58 | def test_ast_compare_diff_top_nested(): 59 | a = str2ast(""" 60 | def foo(): 61 | a = 1 62 | """) 63 | b = str2ast(""" 64 | def foo(): 65 | b = 1 66 | """) 67 | 68 | assert pnrp.compare_ast(a, b) == False 69 | -------------------------------------------------------------------------------- /tests/test_driver.py: -------------------------------------------------------------------------------- 1 | import pnrp 2 | import inspect 3 | 4 | 5 | def test_driver_basic(): 6 | gvars = {} 7 | fchanges = [ 8 | """ 9 | a = 'hello' 10 | """, """ 11 | a = 'hello' 12 | print(a) 13 | a *= 2 14 | """ 15 | ] 16 | 17 | fchanges = [('run.py', inspect.cleandoc(x)) for x in fchanges] 18 | pnrp.driver(fchanges, gvars=gvars) 19 | 20 | assert gvars['a'] == 'hellohello' 21 | 22 | 23 | def test_driver_syntax_error(): 24 | """Should continue on after syntax errors""" 25 | gvars = {} 26 | fchanges = [ 27 | """ 28 | a = 'hello' 29 | """, """ 30 | a = 'hello' 31 | b = # Syntax Error 32 | a = 'world' 33 | print(a) 34 | """, """ 35 | a = 'hello' 36 | a = 'world' 37 | print(a) 38 | """ 39 | ] 40 | 41 | fchanges = [('run.py', inspect.cleandoc(x)) for x in fchanges] 42 | pnrp.driver(fchanges, gvars=gvars) 43 | 44 | assert gvars['a'] == 'world' 45 | 46 | 47 | def test_driver_exception(): 48 | """Should continue on after exception""" 49 | gvars = {} 50 | fchanges = [ 51 | """ 52 | a = 'hello' 53 | """, """ 54 | a = 'hello' 55 | raise 'foo' 56 | a = 'world' 57 | """, """ 58 | a = 'hello' 59 | a = 'world' 60 | """ 61 | ] 62 | 63 | fchanges = [('run.py', inspect.cleandoc(x)) for x in fchanges] 64 | pnrp.driver(fchanges, gvars=gvars) 65 | 66 | assert gvars['a'] != 'hello' 67 | assert gvars['a'] == 'world' 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Pete and Repeat

2 | 3 |

4 | Love the Python REPL?
5 | Hate that you can't write real software in a Jupyter Notebook?
6 | Well do I have the project for you.
7 |

8 | 9 |

10 | 11 |

12 | 13 | Pete and Repeat (pnrp) brings all the goodness of the REPL and Jupyter Notebooks to your regular old Python projects. 14 | It kind of works like [Observable](https://observablehq.com) but for Python from the CLI. 15 | 16 | To get started run, 17 | 18 | ``` 19 | $ pip install pnrp 20 | $ python -m pnrp .py 21 | ``` 22 | 23 | pnrp will run your script to completion just as the regular Python interpreter. 24 | Instead of exiting though, it'll monitor every line of code executed for changes. 25 | When a line of code changes, it will rerun **ONLY** the statements that have changed, and their dependent statements. 26 | We are able to do this by monitoring the files of any imported module, analyzing the AST of the code to be executed, and tracking dependencies between statements. 27 | This is of course no silver bullet, Python is a highly dynamic language and there are situations when this won't work. 28 | That is why we'll provide you an escape hatch to rerun from specific points in your script onward. 29 | 30 | ## How it works, by example 31 | 32 | Suppose we have a python program 33 | 34 | ```python 35 | # Script at time t 36 | 37 | data = open('some-big-file.txt', 'r').read() 38 | print(len(data)) 39 | ``` 40 | 41 | We then modify the script, 42 | 43 | ```python 44 | # Script at time t+1 45 | import re 46 | 47 | expr = re.compile('Pete (and)? repeat') 48 | 49 | data = open('some-big-file.txt', 'r').read() 50 | lines = data.split('\n') 51 | print(len([x for x in lines if expr.match(x)])) 52 | ``` 53 | 54 | Rerunning the script normally would cause our machine to reread `some-big-file.txt` on each iteration. 55 | With pnrp, we intelligently look at your code to see that line 6 is unaltered so we cache the computation and only run the lines that were changed. 56 | 57 | 58 | ## Feature Comparison 59 | 60 | 61 | | | Live Kernel | Graphics Support | Multi-file Reloading | Bring your own Editor | Normal Execution Order | 62 | |:----------------:|:------------:|:----------------:|:--------------------:|:---------------------:|:----------------------:| 63 | | REPL | X | | | | X | 64 | | Jupyter Notebook | X | X | | | | 65 | | PNRP | X | X | X | X | X | 66 | 67 | 68 | ## TODO 69 | 70 | - [ ] Multi-file / import support 71 | - [X] AST Based statement parsing 72 | - [X] AST Dependency based rerun 73 | - [ ] Explore [`sys.settrace`](https://docs.python.org/3/library/sys.html#sys.settrace) for code dependencies 74 | - [ ] Pretty printing 75 | - [ ] Server / Client model ?? (to better support interactivity, interrupts, etc.) 76 | -------------------------------------------------------------------------------- /tests/test_ast_diff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the ast diffing engine to tell us which statements should be executed. 3 | These tests are generally of the form, 4 | 5 | diff(curr_ast, next_ast) == statements to execute 6 | 7 | """ 8 | 9 | import ast 10 | import inspect 11 | import pnrp 12 | import pytest 13 | 14 | 15 | def str2ast(x): 16 | return ast.parse(inspect.cleandoc(x)).body 17 | 18 | 19 | def ast2str(x): 20 | return ast.unparse(x) 21 | 22 | 23 | def test_nothing_to_run(): 24 | curr_ast = str2ast(""" 25 | a = 1 26 | """) 27 | next_ast = str2ast(""" 28 | a = 1 29 | """) 30 | 31 | to_run = pnrp.exprs_to_run(curr_ast, next_ast) 32 | 33 | assert to_run == [] 34 | 35 | 36 | def test_run_everything_after(): 37 | curr_ast = str2ast(""" 38 | a = 1 39 | """) 40 | next_ast = str2ast(""" 41 | a = 1 42 | b = 2 43 | c = 3 44 | """) 45 | 46 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 47 | 48 | assert ast2str(exprs) == inspect.cleandoc(""" 49 | b = 2 50 | c = 3 51 | """) 52 | 53 | 54 | def test_run_everything_after_function(): 55 | curr_ast = str2ast(""" 56 | a = 1 57 | """) 58 | next_ast = str2ast( 59 | """ 60 | a = 1 61 | def foo(x): 62 | return x + 1 63 | foo(a) 64 | """ 65 | ) 66 | 67 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 68 | 69 | assert ast2str(exprs) == inspect.cleandoc( 70 | """ 71 | def foo(x): 72 | return x + 1 73 | foo(a) 74 | """ 75 | ) 76 | 77 | 78 | def test_rerun_dependent_exprs(): 79 | curr_ast = str2ast( 80 | """ 81 | a = 1 82 | def foo(x): 83 | return x + 1 84 | foo(a) 85 | """ 86 | ) 87 | next_ast = str2ast( 88 | """ 89 | a = 1 90 | def foo(x): 91 | return x + 2 92 | foo(a) 93 | """ 94 | ) 95 | 96 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 97 | 98 | assert ast2str(exprs) == inspect.cleandoc( 99 | """ 100 | def foo(x): 101 | return x + 2 102 | foo(a) 103 | """ 104 | ) 105 | 106 | 107 | def test_rerun_dependent_exprs_assign(): 108 | curr_ast = str2ast( 109 | """ 110 | a = 1 111 | def foo(x): 112 | return x + 1 113 | foo(a) 114 | """ 115 | ) 116 | next_ast = str2ast( 117 | """ 118 | a = 2 119 | def foo(x): 120 | return x + 1 121 | foo(a) 122 | """ 123 | ) 124 | 125 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 126 | 127 | assert ast2str(exprs) == inspect.cleandoc(""" 128 | a = 2 129 | foo(a) 130 | """) 131 | 132 | 133 | def test_rerun_grand_dependent_exprs_assign(): 134 | curr_ast = str2ast( 135 | """ 136 | a = 1 137 | b = 2 138 | c = a + b 139 | d = c + 1 140 | e = b + 1 141 | """ 142 | ) 143 | next_ast = str2ast( 144 | """ 145 | a = 2 146 | b = 2 147 | c = a + b 148 | d = c + 1 149 | e = b + 1 150 | """ 151 | ) 152 | 153 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 154 | 155 | assert ast2str(exprs) == inspect.cleandoc( 156 | """ 157 | a = 2 158 | c = a + b 159 | d = c + 1 160 | """ 161 | ) 162 | 163 | 164 | def test_run_independent_new_exprs_only(): 165 | curr_ast = str2ast(""" 166 | a = 1 167 | """) 168 | next_ast = str2ast(""" 169 | import time 170 | a = 1 171 | time.sleep(1) 172 | """) 173 | 174 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 175 | 176 | assert ast2str(exprs) == inspect.cleandoc(""" 177 | import time 178 | time.sleep(1) 179 | """) 180 | 181 | 182 | def test_handles_all_assignments(): 183 | curr_ast = str2ast("") 184 | next_ast = str2ast(""" 185 | a = 1 186 | (a, b) = (2, 3) 187 | """) 188 | 189 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 190 | 191 | assert ast2str(exprs) == inspect.cleandoc(""" 192 | a = 1 193 | (a, b) = (2, 3) 194 | """) 195 | 196 | 197 | def test_handles_print_delete_and_readd(): 198 | curr_ast = str2ast(""" 199 | a = 1 200 | print(a) 201 | """) 202 | next_ast = str2ast(""" 203 | a = 2 204 | print(a) 205 | """) 206 | 207 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 208 | 209 | assert ast2str(exprs) == inspect.cleandoc(""" 210 | a = 2 211 | print(a) 212 | """) 213 | 214 | curr_ast = next_ast 215 | next_ast = str2ast(""" 216 | a = 2 217 | """) 218 | 219 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 220 | 221 | assert ast2str(exprs) == inspect.cleandoc("") 222 | 223 | curr_ast = next_ast 224 | next_ast = str2ast(""" 225 | a = 2 226 | print(a) 227 | """) 228 | 229 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 230 | 231 | assert ast2str(exprs) == inspect.cleandoc(""" 232 | print(a) 233 | """) 234 | 235 | 236 | def test_handles_aug_assign(): 237 | curr_ast = str2ast(""" 238 | a = 1 239 | print(a) 240 | """) 241 | next_ast = str2ast(""" 242 | a = 1 243 | print(a) 244 | a += 2 245 | """) 246 | 247 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 248 | 249 | assert ast2str(exprs 250 | ) == inspect.cleandoc(""" 251 | a = 1 252 | print(a) 253 | a += 2 254 | """) 255 | 256 | 257 | def test_handles_ann_assign(): 258 | curr_ast = str2ast(""" 259 | a = 1 260 | print(a) 261 | """) 262 | next_ast = str2ast(""" 263 | a = 1 264 | print(a) 265 | a: int = 2 266 | """) 267 | 268 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 269 | 270 | assert ast2str(exprs) == inspect.cleandoc( 271 | """ 272 | a = 1 273 | print(a) 274 | a: int = 2 275 | """ 276 | ) 277 | 278 | 279 | # We'll come back to this later, good test though 280 | @pytest.mark.skip 281 | def test_handles_overwriting_variables(): 282 | curr_ast = str2ast(""" 283 | a = 1 284 | print(a) 285 | """) 286 | next_ast = str2ast( 287 | """ 288 | a = 1 289 | print(a) 290 | a = 2 291 | print(a + 1) 292 | """ 293 | ) 294 | 295 | exprs = pnrp.exprs_to_run(curr_ast, next_ast) 296 | 297 | assert ast2str(exprs) == inspect.cleandoc(""" 298 | a = 2 299 | print(a + 1) 300 | """) 301 | -------------------------------------------------------------------------------- /pnrp/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import sys 4 | import time 5 | import importlib 6 | import subprocess 7 | import multiprocessing 8 | import itertools 9 | import ast 10 | import traceback 11 | import difflib 12 | 13 | from typing import List 14 | 15 | 16 | class ANSI: 17 | BLACK = '\u001b[30m' 18 | RED = '\u001b[31m' 19 | GREEN = '\u001b[32m' 20 | YELLOW = '\u001b[33m' 21 | BLUE = '\u001b[34m' 22 | MAGENTA = '\u001b[35m' 23 | CYAN = '\u001b[36m' 24 | WHITE = '\u001b[37m' 25 | DIM = '\u001b[2m' 26 | RESET = '\u001b[0m' 27 | 28 | 29 | prompt = lambda *args: print(ANSI.DIM + '>', *args, ANSI.RESET) 30 | eprint = lambda *args: print(*args, file=sys.stderr) 31 | HOME_DIR = os.path.expanduser('~/.pnrp') 32 | 33 | 34 | def filewatcher(fpath): 35 | fullfpath = os.path.join(os.getcwd(), fpath) 36 | mtime = None 37 | while True: 38 | try: 39 | nmtime = os.stat(fullfpath).st_mtime 40 | except: 41 | continue 42 | 43 | if mtime != nmtime: 44 | mtime = nmtime 45 | 46 | if os.path.exists(fullfpath): 47 | yield fullfpath, open(fullfpath, 'r').read() 48 | 49 | time.sleep(0.5) 50 | 51 | 52 | def compare_ast(node1, node2): 53 | if type(node1) != type(node2): 54 | return False 55 | elif isinstance(node1, ast.AST): 56 | for kind, var in vars(node1).items(): 57 | if kind not in ('lineno', 'col_offset', 'ctx'): 58 | var2 = vars(node2).get(kind) 59 | if not compare_ast(var, var2): 60 | return False 61 | return True 62 | elif isinstance(node1, list): 63 | if len(node1) != len(node2): 64 | return False 65 | for i in range(len(node1)): 66 | if not compare_ast(node1[i], node2[i]): 67 | return False 68 | return True 69 | else: 70 | return node1 == node2 71 | 72 | 73 | def exprs_to_run(curr_ast: List['Expression'], next_ast: List['Expression']): 74 | """Return the minimal list of statements to run to get the execution 75 | of curr_ast to match a clean execution of next_ast. 76 | 77 | For each expression that has changed, we see which global variables it changes via 78 | assignment, then recursively apply that logic to every unchanged statment that uses 79 | the changes variables. 80 | 81 | This culminates in a list of expressions that have been directly changed, or are 82 | affected by a changed expression. 83 | 84 | WARNING: This does not account for mutations or side effects. It encourages you 85 | to write simple code. Any side-effectful code should be wrapped in a function to 86 | scope it's side effects. Local mutation is okay! 87 | 88 | We will support an escape hatch to explicitly rerun side effectful code. 89 | """ 90 | # Calculate the new or changed statements to be run 91 | changed_exprs = [] 92 | curr_code_exprs = [ast.dump(x, include_attributes=False) for x in curr_ast] 93 | next_code_exprs = [ast.dump(x, include_attributes=False) for x in next_ast] 94 | diff = difflib.SequenceMatcher(a=curr_code_exprs, b=next_code_exprs) 95 | for tag, i1, i2, j1, j2 in diff.get_opcodes(): 96 | if tag == 'replace': 97 | changed_exprs.append(next_ast[j1:j2]) 98 | if tag == 'insert': 99 | changed_exprs.append(next_ast[j1:j2]) 100 | 101 | changed_exprs = [x for slst in changed_exprs for x in slst] 102 | 103 | def extract_assignments(expr): 104 | """Given an expression, extract all the top-level variable reassignments 105 | that occur. A list of all assignment syntax can be found at 106 | 107 | https://docs.python.org/3/library/ast.html#abstract-grammar 108 | """ 109 | if isinstance(expr, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): 110 | return [expr.name] 111 | elif isinstance(expr, (ast.Assign,)): 112 | return [expr for sexpr in expr.targets for expr in extract_assignments(sexpr)] 113 | elif isinstance(expr, (ast.AugAssign, ast.AnnAssign)): 114 | return extract_assignments(expr.target) 115 | elif isinstance(expr, (ast.Name,)): 116 | return expr.id 117 | elif isinstance(expr, (ast.List, ast.Tuple)): 118 | return [expr for sexpr in expr.elts for expr in extract_assignments(sexpr)] 119 | 120 | return [] 121 | 122 | # Extract the global variables that will change directly 123 | # as a result of the code changes 124 | changed_names = set() 125 | for expr in changed_exprs: 126 | changed_names |= set(extract_assignments(expr)) 127 | 128 | # Select every expression that either directly changed itself 129 | # or, depends on a global variable that was changed, recursively. 130 | exprs = [] 131 | for expr in next_ast: 132 | if expr in changed_exprs: 133 | exprs.append(expr) 134 | continue 135 | 136 | wexpr = ast.Module(body=[expr], type_ignores=[]) 137 | co = compile(wexpr, '', 'exec') 138 | names = set(co.co_names) 139 | if names & changed_names: 140 | exprs.append(expr) 141 | changed_names |= set(extract_assignments(expr)) 142 | 143 | return exprs 144 | 145 | 146 | def flush_runstate(run_state): 147 | """Write out the runstate so code editors can read it and 148 | present the results inline with the code""" 149 | with open(f'{HOME_DIR}/runstate', 'w') as f: 150 | f.write('\n'.join(run_state)) 151 | 152 | 153 | def driver(fchanges, gvars=None, flush_runstate=flush_runstate): 154 | curr_ast = [] 155 | gvars = {} if gvars is None else gvars 156 | 157 | for fpath, srccode in fchanges: 158 | modname, _ = fpath.rsplit('.', 1) 159 | 160 | # Parse new code, determine which statements to run 161 | try: 162 | next_ast = ast.parse(srccode).body 163 | except SyntaxError: 164 | traceback.print_exc() 165 | continue 166 | 167 | exprs = exprs_to_run(curr_ast, next_ast) 168 | 169 | # Print out the state of what needs to be run 170 | run_state = ['.'] * (srccode.count('\n') + 1) 171 | for expr in exprs: 172 | num_lines = 1 + (expr.end_lineno - expr.lineno) 173 | run_state[expr.lineno - 1:expr.end_lineno] = ['~'] * num_lines # Mark cahnged 174 | 175 | flush_runstate(run_state) 176 | 177 | # Run the changed expressions 178 | if not exprs: 179 | prompt('File saved, but nothing to run') 180 | curr_ast = next_ast 181 | else: 182 | prompt(fpath, 'Changed!') 183 | 184 | for expr in exprs: 185 | num_lines = 1 + (expr.end_lineno - expr.lineno) 186 | try: 187 | wexpr = ast.Module(body=[expr], type_ignores=[]) 188 | code = compile(wexpr, modname, 'exec') 189 | run_state[expr.lineno - 1:expr.end_lineno] = ['>>'] * num_lines # Mark running 190 | flush_runstate(run_state) 191 | exec(code, gvars) 192 | run_state[expr.lineno - 1:expr.end_lineno] = ['~'] * num_lines # Mark complete 193 | except Exception: 194 | run_state[expr.lineno - 1:expr.end_lineno] = ['x'] * num_lines # Mark failed 195 | flush_runstate(run_state) 196 | traceback.print_exc() 197 | break 198 | else: 199 | curr_ast = next_ast 200 | run_state = ['.'] * (srccode.count('\n') + 1) # Mark all complete 201 | flush_runstate(run_state) 202 | 203 | 204 | def cli(fpath, args): 205 | del sys.argv[0] 206 | # Ensure we have a homedir setup 207 | os.makedirs(HOME_DIR, exist_ok=True) 208 | driver(filewatcher(fpath)) 209 | --------------------------------------------------------------------------------