๐Ÿ๐Ÿ๐Ÿ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

reorganize core submodules; start in on terminal abstraction layer

+616 -384
+1
.gitignore
··· 16 16 build 17 17 *.ipynb 18 18 *.ipynb.py 19 + .TODO 19 20
+1 -7
pyt/core/__init__.py
··· 8 8 manually nesting loops and try-catch blocks. 9 9 """ 10 10 11 - from .core import AttrDict, Errs, errs, lsnap 12 - 13 - from .logger import Logger 14 - 15 - from .sketch import try_dump_locals 16 - 17 - from .commands import builtin_commands, command_registrar 11 + from .general import * 18 12 19 13 from .session import PytSession 20 14
-325
pyt/core/commands.py
··· 1 - 2 - import shlex 3 - 4 - from argparse import ArgumentParser, ArgumentError 5 - 6 - from pyt.core import lsnap 7 - from pyt.lib.ansi import codes as ac 8 - 9 - builtin_commands = [] 10 - 11 - def command_registrar(command_group): 12 - def register_command(*aliases, arg_parser=None): 13 - aliases = list(aliases) 14 - if any(" " in alias for alias in aliases): 15 - raise RuntimeError("spaces are not permitted in command aliases!") 16 - def _register_command(behavior): 17 - _behavior = behavior 18 - if isinstance(arg_parser, ArgumentParser): 19 - arg_parser.exit_on_error = False 20 - def _behavior(session, args): 21 - try: 22 - _args = arg_parser.parse_args(shlex.split(args)) 23 - except ArgumentError: 24 - session.log(arg_parser.format_usage(), mode="info", indent=4) 25 - raise 26 - return behavior(session, _args) 27 - command_group.append((aliases, _behavior)) 28 - return behavior 29 - return _register_command 30 - return register_command 31 - 32 - _builtin = command_registrar(builtin_commands) 33 - 34 - test_parser = ArgumentParser("test") 35 - test_parser.add_argument("--foo", dest="bar", type=str, default="baz") 36 - 37 - @_builtin("test", arg_parser=test_parser) 38 - def cmd_test(session, args): 39 - print(args.bar) 40 - 41 - new_parser = ArgumentParser("new") 42 - new_parser.add_argument("name", type=str, help="Name of the new sketch") 43 - new_parser.add_argument("--template", "-t", type=str, default=None, 44 - help="Template to use (verbose, basic, or path to file)") 45 - 46 - @_builtin("new", arg_parser=new_parser) 47 - def cmd_new(session, args): 48 - import shutil 49 - from pathlib import Path 50 - 51 - log = session.log 52 - 53 - template = args.template or session.env.get("TEMPLATE") or "verbose" 54 - 55 - if template in ["verbose", "basic"]: 56 - from importlib import import_module 57 - template_module = import_module(f"pyt.core.templates.{template}") 58 - template = Path(template_module.__file__) 59 - elif isinstance(template, Path): 60 - if not template.exists(): 61 - log(f"template file not found: {template}", mode="error") 62 - return 63 - elif isinstance(template, str): 64 - try: 65 - template = Path(template) 66 - except: 67 - log("template could not be interpreted as a path", mode="error") 68 - return 69 - if not template.exists(): 70 - log(f"template file not found: {template}", mode="error") 71 - return 72 - else: 73 - log("template could not be interpreted as a path", mode="error") 74 - return 75 - 76 - sketch_dir = session.env.SKETCH 77 - new_sketch = sketch_dir / f"{args.name}.py" 78 - 79 - if new_sketch.exists(): 80 - log(f"sketch {args.name} already exists", mode="error") 81 - return 82 - 83 - shutil.copy(template, new_sketch) 84 - sketch_link = ac.file_link(new_sketch) 85 - template_link = ac.file_link(template) 86 - 87 - log(f"created {sketch_link} from template {template_link}", mode="ok") 88 - 89 - @_builtin("flush") 90 - def cmd_flush(session, args): 91 - import gc 92 - import sys 93 - session.persistent_state = {} 94 - session.persistent_hashes = {} 95 - gc.collect() 96 - if "torch" in sys.modules: 97 - torch = sys.modules["torch"] 98 - if torch.cuda.is_available(): 99 - torch.cuda.empty_cache() 100 - session.log("state flushed") 101 - 102 - @_builtin("exit", "quit", ":q", ",q") 103 - def cmd_exit(session, args): 104 - session.log.blank().log("goodbye <3").blank() 105 - session.repl_continue = False 106 - 107 - @_builtin("hello", "hi") 108 - def cmd_hi(session, args): 109 - session.log("hiii :3") 110 - 111 - @_builtin("reload", "refresh", "rr") 112 - def cmd_reload(session, args): 113 - import sys 114 - from importlib import reload 115 - 116 - log = session.log 117 - 118 - session_reload = False 119 - 120 - # TODO find a robust way to track which files have actually changed 121 - for name, module in list(sys.modules.items()): 122 - if name.startswith("pyt"): 123 - try: 124 - reload(module) 125 - log(f"reloaded {name}") 126 - if name == "pyt.core.session": 127 - session_reload = True 128 - except (KeyboardInterrupt, SystemExit): 129 - raise 130 - except: 131 - log.indented().trace() 132 - log(f"failed to reload {name}", mode="error") 133 - 134 - if session_reload: 135 - new_session_module = sys.modules["pyt.core.session"] 136 - session.update_class(new_session_module.PytSession) 137 - 138 - 139 - @_builtin("rrun") 140 - def cmd_reload_run(session, args): 141 - if session.try_handle_command("reload", ""): 142 - session.handle_message(f"run {args}") 143 - 144 - @_builtin("crash") 145 - def cmd_crash(session, args): 146 - raise Exception("crashing on purpose >:3") 147 - 148 - @_builtin("bash", ";") 149 - def cmd_bash(session, args): 150 - import os 151 - import subprocess 152 - 153 - log = session.log 154 - 155 - if args == "": 156 - log("switching to bash!", mode="info") 157 - subprocess.run(["bash"]) 158 - log("wb bestie!") 159 - return 160 - 161 - args = session.favorite_dirs.get(args, args) 162 - 163 - path = os.path.expandvars(os.path.expanduser(args)) 164 - 165 - if not os.path.isdir(path): 166 - log(f"{args} is not a directory but that's ok", mode="warning") 167 - log(f"switching to bash. home directory", mode="info") 168 - subprocess.run(["bash"]) 169 - log("welcome back") 170 - else: 171 - log(f"switching to bash, working directory {path}", mode="info") 172 - subprocess.run(["bash"], cwd=path) 173 - log("back in the pyt :D") 174 - 175 - @_builtin("faves", "ff") 176 - def cmd_faves(session, args): 177 - from pyt.lib.ansi import codes as ac 178 - links = ["Favorite Directories:"] 179 - dirs = session.favorite_dirs 180 - for key in dirs.keys(): 181 - cyan = ac.ansi(ac.fg('cyan')) 182 - links.append(" " + ac.file_link(dirs[key], text=f"{cyan}{key}{ac.reset} -> {dirs[key]}")) 183 - session.log("\n".join(links)) 184 - 185 - def _python_subprocess(session): 186 - import subprocess 187 - 188 - from pyt.lib.ansi import codes as ac 189 - 190 - log = session.log 191 - 192 - log("switching to python!", mode="info") 193 - 194 - if session.env.PYTHON_PATH != None: 195 - try: 196 - subprocess.run([session.env.PYTHON_PATH, "-q"]) 197 - except FileNotFoundError: 198 - link = ac.link(f"file://{session.env.PYTHON_PATH}", "preferred python") 199 - log(f"your {link} didn't load. trying system python :/", mode="warning") 200 - subprocess.run(["python", "-q"]) 201 - else: 202 - subprocess.run(["python", "-q"]) 203 - 204 - log("wb bestie!") 205 - 206 - 207 - 208 - def _python_stateful(session): 209 - log = session.log 210 - log("entering python mode. persistent state is available", mode="info") 211 - 212 - state = dict(session.persistent_state) 213 - state["session"] = session 214 - state["print"] = log.tag("python") 215 - state["_print"] = print 216 - 217 - from pyt.core.pywrapl import repl 218 - 219 - # TODO on_version_mismatch from pytrc 220 - repl(local=state, log=log, on_version_mismatch="warning") 221 - 222 - log("back to snakepyt :)") 223 - 224 - @_builtin("python", "py", "'") 225 - def cmd_python(session, args): 226 - if args == "fresh": 227 - _python_subprocess(session) 228 - else: 229 - if args != "": 230 - session.log("i dunno what u expect me to do w/ that argument lol", mode="info") 231 - _python_stateful(session) 232 - 233 - @_builtin("run") 234 - def cmd_run(session, args): 235 - import os 236 - import sys 237 - import inspect 238 - import time 239 - import shutil 240 - from pathlib import Path 241 - from time import perf_counter 242 - from importlib import import_module 243 - 244 - from pyt.core.run import run, handle_persistent 245 - 246 - sketch_name, remainder = lsnap(args) 247 - 248 - log = session.log 249 - 250 - try: 251 - log(f"loading sketch \"{sketch_name}\"", mode="info") 252 - module_name = f"{sketch_name}" 253 - if module_name in sys.modules: 254 - del sys.modules[module_name] 255 - sys.path.insert(0, str(session.env.SKETCH)) 256 - sketch = import_module(module_name) 257 - sys.path.pop(0) 258 - except ModuleNotFoundError: 259 - log.indented().trace() 260 - log("no such sketch", mode="error", indent=4).blank() 261 - return 262 - except KeyboardInterrupt: 263 - log("aborted", mode="info").blank() 264 - return 265 - except SystemExit: 266 - raise 267 - except: 268 - log.indented().trace() 269 - return 270 - 271 - sources = { name : inspect.getsource(member) 272 - for name, member in inspect.getmembers(sketch) 273 - if inspect.isfunction(member) and member.__module__ == module_name 274 - } 275 - 276 - log = log.tag(sketch_name).mode("info") 277 - 278 - t0 = perf_counter() 279 - if hasattr(sketch, "persistent"): 280 - if not handle_persistent(session, sketch_name, sketch.persistent, sketch.__dict__, log, sources): 281 - log.blank() 282 - return 283 - 284 - sketch.__dict__.update(session.persistent_state) 285 - 286 - pyt_out = session.env.OUT 287 - 288 - if not pyt_out: 289 - session.log("no output directory has been specified.\nset via --out flag, or session.env.OUT in your ~/.config/pytrc.py, or by setting the PYT_OUT environment variable", mode="error") 290 - session.log("aborting.", mode="error") 291 - return 292 - 293 - daily = time.strftime("%d.%m.%Y") 294 - moment = time.strftime("t%H.%M.%S") 295 - 296 - run_dir = Path(os.path.join(pyt_out, sketch_name, daily, moment)) 297 - sketch.__dict__["run_dir"] = run_dir 298 - run_dir.mkdir(parents=True, exist_ok=True) 299 - 300 - shutil.copy(sketch.__file__, run_dir / f"{sketch_name}.py") 301 - 302 - with open(run_dir / f".snakepyt", "w") as metadata: 303 - metadata.write(f"snakepyt version {session.snakepyt_version[0]}.{session.snakepyt_version[1]}\n") 304 - 305 - if hasattr(sketch, "main"): 306 - if hasattr(sketch, "final"): 307 - finalizer = sketch.final.__code__ 308 - else: 309 - finalizer = None 310 - try: 311 - failures, runs = run(session, sketch.main, None, (), sketch.__dict__, log, sources, finalizer) 312 - except KeyboardInterrupt: 313 - log.blank().log("aborted", mode="info").blank() 314 - return 315 - else: 316 - log("sketch has no main function", mode="error", indent=4) 317 - 318 - if failures == 0: 319 - log(f"finished {runs} run(s) in {perf_counter() - t0:.3f}s", mode="ok") 320 - elif failures < runs: 321 - log(f"finished {runs-failures} of {runs} run(s) in {perf_counter() - t0:.3f}s", mode="info") 322 - log(f"{failures} run(s) failed to finish", mode="error") 323 - else: 324 - log(f"all {failures} run(s) failed to finish", mode="error") 325 -
+12
pyt/core/commands/__init__.py
··· 1 + 2 + from .commands import registrar_attr, register_builtins as core_builtins 3 + 4 + from .sketch import register_builtins as sketch_builtins 5 + 6 + from ._test import register_builtins as _test_builtins 7 + 8 + def register_builtins(group): 9 + core_builtins(group) 10 + sketch_builtins(group) 11 + _test_builtins(group) 12 +
+47
pyt/core/commands/_test.py
··· 1 + 2 + from argparse import ArgumentParser 3 + 4 + from pyt.core.commands.commands import registrar_attr 5 + 6 + _builtins = [] 7 + _builtin = registrar_attr(_builtins) 8 + 9 + def register_builtins(group): 10 + group += _builtins 11 + 12 + @_builtin("kitty") 13 + def cmd_kitty(session, args): 14 + import sys, os, termios, tty, select 15 + 16 + timeout = 0.1 17 + 18 + if not sys.stdin.isatty(): 19 + return False 20 + 21 + fd = sys.stdin.fileno() 22 + old = termios.tcgetattr(fd) 23 + try: 24 + tty.setraw(fd) 25 + os.write(sys.stdout.fileno(), b"\x1b[?u") # ask for kitty flags 26 + 27 + if not select.select([fd], [], [], timeout)[0]: 28 + return False 29 + 30 + resp = os.read(fd, 16) 31 + print(resp.startswith(b"\x1b[?") and resp.endswith(b"u")) 32 + return 33 + finally: 34 + termios.tcsetattr(fd, termios.TCSADRAIN, old) 35 + print(False) 36 + 37 + test_parser = ArgumentParser("test") 38 + test_parser.add_argument("--foo", dest="bar", type=str, default="baz") 39 + 40 + @_builtin("test", arg_parser=test_parser) 41 + def cmd_test(session, args): 42 + session.log(args.bar) 43 + 44 + @_builtin("crash") 45 + def cmd_crash(session, args): 46 + raise Exception("crashing on purpose >:3") 47 +
+191
pyt/core/commands/commands.py
··· 1 + 2 + import shlex 3 + 4 + from argparse import ArgumentParser, ArgumentError 5 + 6 + from pyt.core import lsnap 7 + from pyt.core.terminal.ansi import codes as ac 8 + 9 + def _apply_parser(behavior, parser): 10 + parser.exit_on_error = False 11 + def _behavior(session, args): 12 + try: 13 + _args = parser.parse_args(shlex.split(args)) 14 + behavior(session, _args) 15 + except ArgumentError: 16 + session.log(parser.format_usage(), mode="info", indent=4) 17 + raise 18 + return _behavior 19 + 20 + def register(group, behavior, *aliases, arg_parser=None): 21 + if any(" " in alias for alias in aliases): 22 + raise RuntimeError("spaces are not permitted in command aliases!") 23 + 24 + if isinstance(arg_parser, ArgumentParser): 25 + behavior = _apply_parser(behavior, arg_parser) 26 + 27 + group.append((list(aliases), behavior)) 28 + 29 + # looks arcane but it's just chopping register up for attribute-style usage: 30 + # cmd = registar_attr(some_cmd_group) 31 + # @cmd("some", "aliases", arg_parser=ap) 32 + # def foo: ... 33 + registrar_attr = lambda g: lambda *a, arg_parser=None: lambda b: register(g, b, *a, arg_parser=arg_parser) 34 + 35 + _builtins = [] 36 + _builtin = registrar_attr(_builtins) 37 + 38 + def register_builtins(group): 39 + group += _builtins 40 + 41 + @_builtin("exit", "quit", ":q", ",q") 42 + def _exit(session, args): 43 + from pyt.core.terminal import persona 44 + session.log.blank().log(f"goodbye {persona.smile()}").blank() 45 + session.repl_continue = False 46 + 47 + @_builtin("cmds") 48 + def _commands(session, args): 49 + from pyt.core.terminal.ansi import codes as ac 50 + fg = ac.ansi(ac.fg("cyan")) 51 + bg = ac.ansi(ac.fg("cyan"), ac.mode("dim")) 52 + res = "commands:\n" + "\n".join([f" {bg}aka{ac.reset} ".join(f'{bg}"{ac.reset}{fg}' + y + f'{ac.reset}{bg}"{ac.reset}' for y in x[0]) for x in session.commands.all_available]) 53 + session.log(res) 54 + 55 + @_builtin("hello", "hi") 56 + def _hi(session, args): 57 + from pyt.core.terminal import persona 58 + session.log(f"{persona.hello()} {persona.smile()}") 59 + 60 + @_builtin("reload", "refresh", "rr") 61 + def _reload(session, args): 62 + import sys 63 + from importlib import reload 64 + 65 + log = session.log 66 + 67 + session_reload = False 68 + 69 + # TODO find a robust way to track which files have actually changed 70 + for name, module in list(sys.modules.items()): 71 + if name.startswith("pyt"): 72 + try: 73 + reload(module) 74 + log(f"reloaded {name}") 75 + if name == "pyt.core.session": 76 + session_reload = True 77 + except (KeyboardInterrupt, SystemExit): 78 + raise 79 + except: 80 + log.indented().trace() 81 + log(f"failed to reload {name}", mode="error") 82 + 83 + if session_reload: 84 + new_session_module = sys.modules["pyt.core.session"] 85 + session.update_class(new_session_module.PytSession) 86 + 87 + @_builtin("prefix", "pfx", "pre") 88 + def _prefix(session, args): 89 + if args == ";": 90 + session.try_handle_command("bash", "") 91 + return 92 + 93 + session.prefix = args 94 + 95 + @_builtin("do", ";") 96 + def _bash_do(session, args): 97 + import subprocess 98 + 99 + subprocess.run( 100 + ["bash", "-ic", args] 101 + ) 102 + 103 + @_builtin("bash", ":") 104 + def _bash(session, args): 105 + import os 106 + import subprocess 107 + 108 + from pyt.core.terminal import persona 109 + 110 + log = session.log 111 + 112 + if args == "": 113 + log("switching to bash!", mode="info") 114 + subprocess.run(["bash"]) 115 + log("wb bestie!") 116 + return 117 + 118 + args = session.favorite_dirs.get(args, args) 119 + 120 + path = os.path.expandvars(os.path.expanduser(args)) 121 + 122 + if not os.path.isdir(path): 123 + log(f"{args} is not a directory but that's ok", mode="warning") 124 + log(f"switching to bash. home directory", mode="info") 125 + subprocess.run(["bash"]) 126 + log("welcome back") 127 + else: 128 + log(f"switching to bash, working directory {path}", mode="info") 129 + subprocess.run(["bash"], cwd=path) 130 + log(f"back in the pyt {persona.smile()}") 131 + 132 + @_builtin("faves", "ff") 133 + def _faves(session, args): 134 + from pyt.core.terminal.ansi import codes as ac 135 + links = ["Favorite Directories:"] 136 + dirs = session.favorite_dirs 137 + for key in dirs.keys(): 138 + cyan = ac.ansi(ac.fg('cyan')) 139 + links.append(" " + ac.file_link(dirs[key], text=f"{cyan}{key}{ac.reset} -> {dirs[key]}")) 140 + session.log("\n".join(links)) 141 + 142 + def _python_subprocess(session): 143 + import subprocess 144 + 145 + from pyt.core.terminal.ansi import codes as ac 146 + 147 + log = session.log 148 + 149 + log("switching to python!", mode="info") 150 + 151 + if session.env.PYTHON_PATH != None: 152 + try: 153 + subprocess.run([session.env.PYTHON_PATH, "-q"]) 154 + except FileNotFoundError: 155 + link = ac.link(f"file://{session.env.PYTHON_PATH}", "preferred python") 156 + log(f"your {link} didn't load. trying system python :/", mode="warning") 157 + subprocess.run(["python", "-q"]) 158 + else: 159 + subprocess.run(["python", "-q"]) 160 + 161 + log("wb bestie!") 162 + 163 + def _python_stateful(session): 164 + from pyt.core.terminal import persona 165 + 166 + log = session.log 167 + log("entering python mode. persistent state is available", mode="info") 168 + 169 + state = dict(session.persistent_state) 170 + state["session"] = session 171 + state["print"] = log.tag("python") 172 + state["_print"] = print 173 + 174 + from pyt.core.terminal.pywrapl import repl 175 + 176 + # TODO on_version_mismatch from pytrc 177 + repl(local=state, log=log, on_version_mismatch="warning") 178 + 179 + log(f"back to snakepyt {persona.smile()}") 180 + 181 + @_builtin("python", "py", "'") 182 + def _python(session, args): 183 + from pyt.core.terminal import persona 184 + 185 + if args == "fresh": 186 + _python_subprocess(session) 187 + else: 188 + if args != "": 189 + session.log(f"i dunno what u expect me to do w/ that argument {persona.laugh()}", mode="info") 190 + _python_stateful(session) 191 +
+175
pyt/core/commands/sketch.py
··· 1 + 2 + from argparse import ArgumentParser 3 + 4 + from pyt.core.commands.commands import registrar_attr 5 + 6 + _builtins = [] 7 + _builtin = registrar_attr(_builtins) 8 + 9 + def register_builtins(group): 10 + group += _builtins 11 + 12 + @_builtin("flush") 13 + def _flush_state(session, args): 14 + import gc 15 + import sys 16 + session.persistent_state = {} 17 + session.persistent_hashes = {} 18 + gc.collect() 19 + if "torch" in sys.modules: 20 + torch = sys.modules["torch"] 21 + if torch.cuda.is_available(): 22 + torch.cuda.empty_cache() 23 + session.log("state flushed") 24 + 25 + new_parser = ArgumentParser("new") 26 + new_parser.add_argument("name", type=str, help="Name of the new sketch") 27 + new_parser.add_argument("--template", "-t", type=str, default=None, 28 + help="Template to use (verbose, basic, or path to file)") 29 + 30 + @_builtin("new", arg_parser=new_parser) 31 + def _new_sketch(session, args): 32 + import shutil 33 + 34 + from pathlib import Path 35 + 36 + from pyt.core.terminal.ansi import codes as ac 37 + 38 + log = session.log 39 + 40 + template = args.template or session.env.get("TEMPLATE") or "verbose" 41 + 42 + if template in ["verbose", "basic"]: 43 + from importlib import import_module 44 + template_module = import_module(f"pyt.core.templates.{template}") 45 + template = Path(template_module.__file__) 46 + elif isinstance(template, Path): 47 + if not template.exists(): 48 + log(f"template file not found: {template}", mode="error") 49 + return 50 + elif isinstance(template, str): 51 + try: 52 + template = Path(template) 53 + except: 54 + log("template could not be interpreted as a path", mode="error") 55 + return 56 + if not template.exists(): 57 + log(f"template file not found: {template}", mode="error") 58 + return 59 + else: 60 + log("template could not be interpreted as a path", mode="error") 61 + return 62 + 63 + sketch_dir = session.env.SKETCH 64 + new_sketch = sketch_dir / f"{args.name}.py" 65 + 66 + if new_sketch.exists(): 67 + log(f"sketch {args.name} already exists", mode="error") 68 + return 69 + 70 + shutil.copy(template, new_sketch) 71 + sketch_link = ac.file_link(new_sketch) 72 + template_link = ac.file_link(template) 73 + 74 + log(f"created {sketch_link} from template {template_link}", mode="ok") 75 + 76 + @_builtin("rrun") 77 + def _reload_run(session, args): 78 + if session.try_handle_command("reload", ""): 79 + session.try_handle_command("run", args) 80 + 81 + @_builtin("run") 82 + def _run(session, args): 83 + import os 84 + import sys 85 + import inspect 86 + import time 87 + import shutil 88 + from pathlib import Path 89 + from time import perf_counter 90 + from importlib import import_module 91 + 92 + from pyt.core.sketch.run import run, handle_persistent 93 + from pyt.core import lsnap 94 + 95 + sketch_name, remainder = lsnap(args) 96 + 97 + log = session.log 98 + 99 + try: 100 + log(f"loading sketch \"{sketch_name}\"", mode="info") 101 + module_name = f"{sketch_name}" 102 + if module_name in sys.modules: 103 + del sys.modules[module_name] 104 + sys.path.insert(0, str(session.env.SKETCH)) 105 + sketch = import_module(module_name) 106 + sys.path.pop(0) 107 + except ModuleNotFoundError: 108 + log.indented().trace() 109 + log("no such sketch", mode="error", indent=4).blank() 110 + return 111 + except KeyboardInterrupt: 112 + log("aborted", mode="info").blank() 113 + return 114 + except SystemExit: 115 + raise 116 + except: 117 + log.indented().trace() 118 + return 119 + 120 + sources = { name : inspect.getsource(member) 121 + for name, member in inspect.getmembers(sketch) 122 + if inspect.isfunction(member) and member.__module__ == module_name 123 + } 124 + 125 + log = log.tag(sketch_name).mode("info") 126 + 127 + t0 = perf_counter() 128 + if hasattr(sketch, "persistent"): 129 + if not handle_persistent(session, sketch_name, sketch.persistent, sketch.__dict__, log, sources): 130 + log.blank() 131 + return 132 + 133 + sketch.__dict__.update(session.persistent_state) 134 + 135 + pyt_out = session.env.OUT 136 + 137 + if not pyt_out: 138 + session.log("no output directory has been specified.\nset via --out flag, or session.env.OUT in your ~/.config/pytrc.py, or by setting the PYT_OUT environment variable", mode="error") 139 + session.log("aborting.", mode="error") 140 + return 141 + 142 + daily = time.strftime("%d.%m.%Y") 143 + moment = time.strftime("t%H.%M.%S") 144 + 145 + run_dir = Path(os.path.join(pyt_out, sketch_name, daily, moment)) 146 + sketch.__dict__["run_dir"] = run_dir 147 + run_dir.mkdir(parents=True, exist_ok=True) 148 + 149 + shutil.copy(sketch.__file__, run_dir / f"{sketch_name}.py") 150 + 151 + with open(run_dir / f".snakepyt", "w") as metadata: 152 + metadata.write(f"snakepyt version {session.snakepyt_version[0]}.{session.snakepyt_version[1]}\n") 153 + 154 + if hasattr(sketch, "main"): 155 + if hasattr(sketch, "final"): 156 + finalizer = sketch.final.__code__ 157 + else: 158 + finalizer = None 159 + failures, runs = 0, 0 160 + try: 161 + failures, runs = run(session, sketch.main, None, (), sketch.__dict__, log, sources, finalizer) 162 + except KeyboardInterrupt: 163 + log.blank().log("aborted", mode="info").blank() 164 + return 165 + else: 166 + log("sketch has no main function", mode="error", indent=4) 167 + 168 + if failures == 0: 169 + log(f"finished {runs} run(s) in {perf_counter() - t0:.3f}s", mode="ok") 170 + elif failures < runs: 171 + log(f"finished {runs-failures} of {runs} run(s) in {perf_counter() - t0:.3f}s", mode="info") 172 + log(f"{failures} run(s) failed to finish", mode="error") 173 + else: 174 + log(f"all {failures} run(s) failed to finish", mode="error") 175 +
+2
pyt/core/core.py pyt/core/general.py
··· 29 29 parts = s.lstrip().split(delimiter, 1) 30 30 return (parts[0].rstrip(), parts[1].lstrip() if len(parts) > 1 else "") 31 31 32 + from dataclasses import dataclass 33 +
+5 -5
pyt/core/logger.py pyt/core/terminal/logger.py
··· 9 9 from typing import Optional 10 10 from pathlib import Path 11 11 12 - from pyt.lib.ansi import codes as ac 12 + from pyt.core.terminal.ansi import codes as ac 13 13 14 14 _tag_colors = { 15 15 "error": ac.ansi(ac.fg("red")), ··· 104 104 print() 105 105 return self 106 106 107 - def input(self, username=None): 108 - if username is None: 109 - username = ":" 110 - return _input(f"{username}:", mode="user", indent=self._indent) 107 + def input(self, fore): 108 + if fore is None: 109 + fore = ":" 110 + return _input(f"{fore}", mode="user", indent=self._indent) 111 111
-22
pyt/core/persona.py
··· 1 - 2 - # TODO: weighted RNG; replace (list str) with (dict str->weight), 3 - # precompute a single looping rng buffer w/ like, 101 entries of randoms in [0,1) 4 - # normalize weights of each dict during startup 5 - # iterator will take the next rng val, run thru the dict keys subtracting their value, 6 - # stop when it hits / passes 0 7 - 8 - _smiles = [":)", ":3", ":D", "c:", "^^", "^_^", "<3"] 9 - _laughs = ["haha", "lol", "lmao", "hehe", "ha"] 10 - _hellos = ["hello", "hi", "hiya", "hey", "hiii"] 11 - 12 - def _iterate(items): 13 - i = 0 14 - n = len(items) 15 - while True: 16 - i = (i + 1) % n 17 - yield items[i] 18 - 19 - smiles = _iterate(_smiles) 20 - laughs = _iterate(_laughs) 21 - hellos = _iterate(_hellos) 22 -
+5 -7
pyt/core/pywrapl.py pyt/core/terminal/pywrapl.py
··· 1 1 2 2 import sys 3 3 4 - _VERSION = { 5 - "major": [3], 6 - "minor": [14], 7 - "micro": [2] 8 - } 4 + _VERSION = [ 5 + (3,14,2) 6 + ] 9 7 10 8 class VersionMismatch(RuntimeError): 11 9 pass ··· 28 26 from _pyrepl import console, simple_interact, readline, trace, historical_reader 29 27 30 28 version = sys.version_info 31 - if not (version.major in _VERSION["major"] and version.minor in _VERSION["minor"] and version.micro in _VERSION["micro"]): 29 + if not ((version.major, version.minor, version.micro) in _VERSION): 32 30 if on_version_mismatch == "error": 33 31 raise VersionMismatch("wrapper for unsupported _pyrepl module was designed around a different version") 34 32 elif on_version_mismatch == "warning": 35 33 log("wrapper for unsupported _pyrepl module was designed around a different version", mode="warning") 36 - elif on_version_mismatch == "ignore": 34 + elif on_version_mismatch != "ignore": 37 35 log("on_version_mismatch should be one of [\"error\", \"warning\", \"ignore\"]", mode="warning") 38 36 39 37 local["exit"] = _ReplExitSentinel()
+13 -4
pyt/core/run.py pyt/core/sketch/run.py
··· 6 6 return (schedule, _schedule) 7 7 8 8 def handle_persistent(session, sketch_name, persistent_fn, module_globals, log, sources): 9 + # TODO refactor to not parse the same shit more than once 10 + import ast 9 11 import hashlib 10 - from pyt.core import try_dump_locals 12 + import inspect 13 + from pyt.core.sketch.sketch import try_dump_locals 14 + 15 + # strip semantically-irrelevant whitespace & comments 16 + canonicalized_source = ast.unparse(ast.parse(sources[persistent_fn.__name__])) 17 + bytecode_hash = hashlib.sha256(canonicalized_source.encode()).hexdigest() 11 18 12 - # TODO better to hash the source code tbh 13 - bytecode_hash = hashlib.sha256(persistent_fn.__code__.co_code).hexdigest() 14 19 if sketch_name in session.persistent_hashes: 15 20 if bytecode_hash == session.persistent_hashes[sketch_name]: 16 21 return True 22 + 17 23 (success, locals_or_err) = try_dump_locals(persistent_fn, 18 24 sources[persistent_fn.__name__], 19 25 [], {}, 20 26 module_globals, log) 27 + 21 28 if success: 22 29 session.persistent_hashes[sketch_name] = bytecode_hash 30 + # TODO: sketches should get their own persistent state dicts, w/ a non-default 31 + # option to persist items into the top-level persistent_state instead 23 32 session.persistent_state.update(locals_or_err) 24 33 else: 25 34 log(f"failed to run persistent function: {locals_or_err}", mode="error") ··· 30 39 def run(session, fn, arg, partial_id, outer_scope, log, sources, finalizer=None): 31 40 from time import perf_counter 32 41 33 - from pyt.core import try_dump_locals 42 + from pyt.core.sketch.sketch import try_dump_locals 34 43 35 44 scope = dict(outer_scope) 36 45 schedule, schedule_fn = establish_scheduler()
+18 -6
pyt/core/session.py
··· 4 4 5 5 from pathlib import Path 6 6 7 - from pyt.lib.ansi import codes as ac 7 + from pyt.core.terminal.ansi import codes as ac 8 8 from pyt.core import AttrDict, lsnap 9 - from pyt.core.commands import command_registrar, builtin_commands 9 + from pyt.core.commands import registrar_attr, register_builtins 10 10 11 11 def _find_pytrc(): 12 12 config_home = os.getenv("XDG_CONFIG_HOME") ··· 49 49 self.snakepyt_version = (0, 2) 50 50 self.repl_continue = True 51 51 52 + self.prefix = None 52 53 53 54 self.favorite_dirs = {} 54 55 55 56 self.persistent_state = {} 56 57 self.persistent_hashes = {} 57 58 58 - from pyt.core import Logger 59 + from pyt.core.terminal import Logger 59 60 self.log = Logger().mode("ok").tag("snakepyt") 60 61 61 62 self.commands = AttrDict() ··· 66 67 67 68 self._get_paths() 68 69 69 - self.commands.all_available = self.commands.user + builtin_commands 70 + self.commands.builtin = [] 71 + 72 + register_builtins(self.commands.builtin) 73 + 74 + self.commands.all_available = self.commands.user + self.commands.builtin 70 75 71 76 def _get_paths(self): 72 77 # priority order: cli > pytrc > env > default ··· 81 86 if isinstance(self.env.OUT, str): self.env.OUT = Path(self.env.OUT) 82 87 if isinstance(self.env.IN, str): self.env.IN = Path(self.env.IN) 83 88 if isinstance(self.env.SKETCH, str): self.env.SKETCH = Path(self.env.SKETCH) 84 - if isinstance(self.env.TEMPLATE, str): self.env.TEMPLATE = Path(self.env.TEMPLATE) 85 89 86 90 if isinstance(self.env.PYTHON_PATH, str): self.env.PYTHON_PATH = Path(self.env.PYTHON_PATH) 87 91 ··· 109 113 110 114 if pytrc.exists(): 111 115 namespace = { 112 - "command": command_registrar(self.commands.user), 116 + "command": registrar_attr(self.commands.user), 113 117 "session": self, 114 118 "print": log 115 119 } ··· 136 140 def handle_message(self, message): 137 141 log = self.log 138 142 try: 143 + if self.prefix: 144 + if message in ["un", "unpre", "unprefix"]: 145 + self.prefix = None 146 + return 147 + message = " ".join([self.prefix, message]) 148 + 139 149 if message.startswith("."): 140 150 if message.rstrip() == ".": 141 151 state_dump = "\n".join([f" {k}: {type(v).__name__}" for k, v in self.persistent_state.items()]) ··· 180 190 181 191 state = self.persistent_state 182 192 hashes = self.persistent_hashes 193 + prefix = self.prefix 183 194 184 195 self.__class__ = new_instance.__class__ 185 196 self.__dict__.clear() ··· 187 198 188 199 self.persistent_state = state 189 200 self.persistent_hashes = hashes 201 + self.prefix = prefix 190 202
+2 -1
pyt/core/sketch.py pyt/core/sketch/sketch.py
··· 2 2 import ast 3 3 import inspect 4 4 5 - from pyt.core.core import Errs, errs 5 + from pyt.core import Errs, errs 6 6 7 7 class ReturnLocalsWalker(ast.NodeTransformer): 8 8 def __init__(self, logger): ··· 88 88 89 89 return (True, new_func) 90 90 91 + # TODO this function signature is a mess 91 92 @errs(_Errs) 92 93 def try_dump_locals(fn, fn_source, args, kwargs, deftime_globals, log): 93 94 (success, fn_or_err) = modify_to_dump_locals(fn, fn_source, deftime_globals, log)
+3
pyt/core/terminal/__init__.py
··· 1 + 2 + from .logger import Logger 3 +
+29
pyt/core/terminal/persona.py
··· 1 + 2 + import random 3 + 4 + _smiles = { 5 + ":)": 8.0, ":3": 5.0, ":D": 4.0, 6 + "c:": 0.3, "^^": 1.0, "^_^": 1.0, 7 + "<3": 3.0, 8 + } 9 + 10 + _laughs = { 11 + "haha": 1.0, "lol": 1.0, "lmao": 1.0, 12 + "hehe": 1.0, "ha": 1.0, 13 + } 14 + 15 + _hellos = { 16 + "hello": 1.0, "hi": 1.0, "hiya": 1.0, 17 + "hey": 1.0, "hiii": 1.0, 18 + } 19 + 20 + def _get_sampler(weighted_strs: dict[str, float]): 21 + keys = list(weighted_strs.keys()) 22 + return lambda: random.choices(keys, weights=weighted_strs.values(), k=1)[0] 23 + 24 + smile = _get_sampler(_smiles) 25 + laugh = _get_sampler(_laughs) 26 + hello = _get_sampler(_hellos) 27 + 28 + maybe_emote = _get_sampler({"": 10.0, **_smiles, **_hellos, "lmao": 0.0}) 29 +
+103
pyt/core/terminal/terminal.py
··· 1 + 2 + from dataclasses import dataclass 3 + 4 + class ColorRGB: 5 + pass 6 + 7 + class Color256: 8 + pass 9 + 10 + class Color216: 11 + pass 12 + 13 + class ColorTerm: 14 + pass 15 + 16 + # TODO: come up with strong type for color and modes, and anything else that gets added here, 17 + # and once all that's done, we can make this a frozen dataclass 18 + class Style: 19 + def __init__(self): 20 + self.color = None 21 + self.modes = [] # list of idk some kinda enum 22 + 23 + class StyleChange: 24 + pass 25 + 26 + def _write(stream, text, style): 27 + # convert style to ansi 28 + # or if u wanna get real fancy, convert all *style changes* to ansi 29 + pass 30 + 31 + def _flush(stream): 32 + pass 33 + 34 + class _MockOut: 35 + # TODO do-nothing methods for fake out stream 36 + pass 37 + 38 + class _MockIn: 39 + # TODO do-nothing methods for fake in stream 40 + pass 41 + 42 + class Terminal: 43 + """ 44 + Abstract terminal 45 + """ 46 + 47 + _in_stream = None 48 + _out_streams = None 49 + 50 + def __init__(self, in_stream, out_streams): 51 + self._in_stream = in_stream if in_stream else _MockIn() 52 + if not out_streams or len(out_streams) == 0: 53 + self._out_streams = { "default": _MockOut() } 54 + else: 55 + self._out_streams = out_streams 56 + if "default" not in self._out_streams: 57 + self._out_streams["default"] = next(iter(out_streams.values())) 58 + 59 + # TODO: figure out what kinds of styling each out stream actually supports 60 + # including fancy kitty extension stuff! speaking of which, also check if we can 61 + # get nice kitty-mode input events 62 + 63 + # construct style filters from this 64 + 65 + # TODO: function that turns (Style | None, Style | None) pair into StyleChange | None 66 + 67 + # TODO: function that turns an iterable of (text, Style | None) pairs 68 + # into an iterable of text | StyleChange 69 + 70 + def write(self, text, style=None, to="default"): 71 + target = self._out_streams.get(to, None) 72 + if target is None: 73 + raise RuntimeError("valid streams: [\"out\", \"err\"]") 74 + if isinstance(text, list): 75 + for entry in text: 76 + _write(target, entry[0], entry[1]) 77 + else: 78 + _write(target, text, style) 79 + _flush(target) 80 + 81 + # TODO default link style 82 + def write_link(self, uri, text, style=None, to="default"): 83 + # need to handle the case where the link is made up of multiple styles ofc. 84 + # just means tacking on a wrapper around text and style and passing them thru to 85 + # write. 86 + # also need to ensure link capability of the stream tho 87 + pass 88 + 89 + def query(self, query, on_result, on_timeout=None): 90 + # wrap ansi queries so that user doesn't have to poll 91 + # maybe also expose a blocking version? idk 92 + pass 93 + 94 + def read(self): 95 + # just the text out of the input queue; kitty makes this easy but oldschool ansi 96 + # code untangling might be a pain 97 + pass 98 + 99 + def get_input_events(self, clear=True): 100 + # access the input event queue. by default, consume it 101 + # maybe add filtering for which kinds of events the caller cares about 102 + pass 103 +
pyt/lib/ansi/__init__.py pyt/core/terminal/ansi/__init__.py
+2
pyt/lib/ansi/codes.py pyt/core/terminal/ansi/codes.py
··· 82 82 k: str(i) for (i,k) in enumerate(_modes) 83 83 } 84 84 85 + def mode(mode): 86 + return _mode_digits[mode] 85 87 86 88 87 89 # cursor shapes (DECSCUSR)
pyt/lib/ansi/tui.py pyt/core/terminal/ansi/tui.py
+7 -7
pyt/repl.py
··· 4 4 from argparse import ArgumentParser 5 5 6 6 from pyt.core import PytSession 7 + from pyt.core.terminal import persona 7 8 8 9 parser = ArgumentParser("snakepyt") 9 10 PytSession.define_cli_args(parser) ··· 15 16 try: 16 17 username = os.getlogin() 17 18 except: 18 - username = None 19 - session.log(f"hello {username}! <3" if username else "hello! <3") 19 + username = "" 20 + 21 + session.log(f"{persona.hello()} {username}! {persona.smile()}" if username else f"{persona.hello()}! {persona.smile()}") 20 22 session.log.blank() 21 23 22 24 while session.repl_continue: 23 25 try: 24 - message = session.log.input(username) 26 + tag = f"{username}: {session.prefix}" if session.prefix else username + ':' 27 + message = session.log.input(tag) 25 28 except (KeyboardInterrupt, EOFError, SystemExit): 26 - session.log.blank().log("goodbye <3").blank() 29 + session.log.blank().log(f"goodbye {persona.smile()}").blank() 27 30 session.repl_continue = False 28 31 continue 29 - 30 - if "prefix" in session.persistent_state: 31 - message = " ".join([session.persistent_state["prefix"], message]) 32 32 33 33 session.handle_message(message.lstrip()) 34 34