···11+22+import shlex
33+44+from argparse import ArgumentParser, ArgumentError
55+66+from pyt.core import lsnap
77+from pyt.core.terminal.ansi import codes as ac
88+99+def _apply_parser(behavior, parser):
1010+ parser.exit_on_error = False
1111+ def _behavior(session, args):
1212+ try:
1313+ _args = parser.parse_args(shlex.split(args))
1414+ behavior(session, _args)
1515+ except ArgumentError:
1616+ session.log(parser.format_usage(), mode="info", indent=4)
1717+ raise
1818+ return _behavior
1919+2020+def register(group, behavior, *aliases, arg_parser=None):
2121+ if any(" " in alias for alias in aliases):
2222+ raise RuntimeError("spaces are not permitted in command aliases!")
2323+2424+ if isinstance(arg_parser, ArgumentParser):
2525+ behavior = _apply_parser(behavior, arg_parser)
2626+2727+ group.append((list(aliases), behavior))
2828+2929+# looks arcane but it's just chopping register up for attribute-style usage:
3030+# cmd = registar_attr(some_cmd_group)
3131+# @cmd("some", "aliases", arg_parser=ap)
3232+# def foo: ...
3333+registrar_attr = lambda g: lambda *a, arg_parser=None: lambda b: register(g, b, *a, arg_parser=arg_parser)
3434+3535+_builtins = []
3636+_builtin = registrar_attr(_builtins)
3737+3838+def register_builtins(group):
3939+ group += _builtins
4040+4141+@_builtin("exit", "quit", ":q", ",q")
4242+def _exit(session, args):
4343+ from pyt.core.terminal import persona
4444+ session.log.blank().log(f"goodbye {persona.smile()}").blank()
4545+ session.repl_continue = False
4646+4747+@_builtin("cmds")
4848+def _commands(session, args):
4949+ from pyt.core.terminal.ansi import codes as ac
5050+ fg = ac.ansi(ac.fg("cyan"))
5151+ bg = ac.ansi(ac.fg("cyan"), ac.mode("dim"))
5252+ 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])
5353+ session.log(res)
5454+5555+@_builtin("hello", "hi")
5656+def _hi(session, args):
5757+ from pyt.core.terminal import persona
5858+ session.log(f"{persona.hello()} {persona.smile()}")
5959+6060+@_builtin("reload", "refresh", "rr")
6161+def _reload(session, args):
6262+ import sys
6363+ from importlib import reload
6464+6565+ log = session.log
6666+6767+ session_reload = False
6868+6969+ # TODO find a robust way to track which files have actually changed
7070+ for name, module in list(sys.modules.items()):
7171+ if name.startswith("pyt"):
7272+ try:
7373+ reload(module)
7474+ log(f"reloaded {name}")
7575+ if name == "pyt.core.session":
7676+ session_reload = True
7777+ except (KeyboardInterrupt, SystemExit):
7878+ raise
7979+ except:
8080+ log.indented().trace()
8181+ log(f"failed to reload {name}", mode="error")
8282+8383+ if session_reload:
8484+ new_session_module = sys.modules["pyt.core.session"]
8585+ session.update_class(new_session_module.PytSession)
8686+8787+@_builtin("prefix", "pfx", "pre")
8888+def _prefix(session, args):
8989+ if args == ";":
9090+ session.try_handle_command("bash", "")
9191+ return
9292+9393+ session.prefix = args
9494+9595+@_builtin("do", ";")
9696+def _bash_do(session, args):
9797+ import subprocess
9898+9999+ subprocess.run(
100100+ ["bash", "-ic", args]
101101+ )
102102+103103+@_builtin("bash", ":")
104104+def _bash(session, args):
105105+ import os
106106+ import subprocess
107107+108108+ from pyt.core.terminal import persona
109109+110110+ log = session.log
111111+112112+ if args == "":
113113+ log("switching to bash!", mode="info")
114114+ subprocess.run(["bash"])
115115+ log("wb bestie!")
116116+ return
117117+118118+ args = session.favorite_dirs.get(args, args)
119119+120120+ path = os.path.expandvars(os.path.expanduser(args))
121121+122122+ if not os.path.isdir(path):
123123+ log(f"{args} is not a directory but that's ok", mode="warning")
124124+ log(f"switching to bash. home directory", mode="info")
125125+ subprocess.run(["bash"])
126126+ log("welcome back")
127127+ else:
128128+ log(f"switching to bash, working directory {path}", mode="info")
129129+ subprocess.run(["bash"], cwd=path)
130130+ log(f"back in the pyt {persona.smile()}")
131131+132132+@_builtin("faves", "ff")
133133+def _faves(session, args):
134134+ from pyt.core.terminal.ansi import codes as ac
135135+ links = ["Favorite Directories:"]
136136+ dirs = session.favorite_dirs
137137+ for key in dirs.keys():
138138+ cyan = ac.ansi(ac.fg('cyan'))
139139+ links.append(" " + ac.file_link(dirs[key], text=f"{cyan}{key}{ac.reset} -> {dirs[key]}"))
140140+ session.log("\n".join(links))
141141+142142+def _python_subprocess(session):
143143+ import subprocess
144144+145145+ from pyt.core.terminal.ansi import codes as ac
146146+147147+ log = session.log
148148+149149+ log("switching to python!", mode="info")
150150+151151+ if session.env.PYTHON_PATH != None:
152152+ try:
153153+ subprocess.run([session.env.PYTHON_PATH, "-q"])
154154+ except FileNotFoundError:
155155+ link = ac.link(f"file://{session.env.PYTHON_PATH}", "preferred python")
156156+ log(f"your {link} didn't load. trying system python :/", mode="warning")
157157+ subprocess.run(["python", "-q"])
158158+ else:
159159+ subprocess.run(["python", "-q"])
160160+161161+ log("wb bestie!")
162162+163163+def _python_stateful(session):
164164+ from pyt.core.terminal import persona
165165+166166+ log = session.log
167167+ log("entering python mode. persistent state is available", mode="info")
168168+169169+ state = dict(session.persistent_state)
170170+ state["session"] = session
171171+ state["print"] = log.tag("python")
172172+ state["_print"] = print
173173+174174+ from pyt.core.terminal.pywrapl import repl
175175+176176+ # TODO on_version_mismatch from pytrc
177177+ repl(local=state, log=log, on_version_mismatch="warning")
178178+179179+ log(f"back to snakepyt {persona.smile()}")
180180+181181+@_builtin("python", "py", "'")
182182+def _python(session, args):
183183+ from pyt.core.terminal import persona
184184+185185+ if args == "fresh":
186186+ _python_subprocess(session)
187187+ else:
188188+ if args != "":
189189+ session.log(f"i dunno what u expect me to do w/ that argument {persona.laugh()}", mode="info")
190190+ _python_stateful(session)
191191+
+175
pyt/core/commands/sketch.py
···11+22+from argparse import ArgumentParser
33+44+from pyt.core.commands.commands import registrar_attr
55+66+_builtins = []
77+_builtin = registrar_attr(_builtins)
88+99+def register_builtins(group):
1010+ group += _builtins
1111+1212+@_builtin("flush")
1313+def _flush_state(session, args):
1414+ import gc
1515+ import sys
1616+ session.persistent_state = {}
1717+ session.persistent_hashes = {}
1818+ gc.collect()
1919+ if "torch" in sys.modules:
2020+ torch = sys.modules["torch"]
2121+ if torch.cuda.is_available():
2222+ torch.cuda.empty_cache()
2323+ session.log("state flushed")
2424+2525+new_parser = ArgumentParser("new")
2626+new_parser.add_argument("name", type=str, help="Name of the new sketch")
2727+new_parser.add_argument("--template", "-t", type=str, default=None,
2828+ help="Template to use (verbose, basic, or path to file)")
2929+3030+@_builtin("new", arg_parser=new_parser)
3131+def _new_sketch(session, args):
3232+ import shutil
3333+3434+ from pathlib import Path
3535+3636+ from pyt.core.terminal.ansi import codes as ac
3737+3838+ log = session.log
3939+4040+ template = args.template or session.env.get("TEMPLATE") or "verbose"
4141+4242+ if template in ["verbose", "basic"]:
4343+ from importlib import import_module
4444+ template_module = import_module(f"pyt.core.templates.{template}")
4545+ template = Path(template_module.__file__)
4646+ elif isinstance(template, Path):
4747+ if not template.exists():
4848+ log(f"template file not found: {template}", mode="error")
4949+ return
5050+ elif isinstance(template, str):
5151+ try:
5252+ template = Path(template)
5353+ except:
5454+ log("template could not be interpreted as a path", mode="error")
5555+ return
5656+ if not template.exists():
5757+ log(f"template file not found: {template}", mode="error")
5858+ return
5959+ else:
6060+ log("template could not be interpreted as a path", mode="error")
6161+ return
6262+6363+ sketch_dir = session.env.SKETCH
6464+ new_sketch = sketch_dir / f"{args.name}.py"
6565+6666+ if new_sketch.exists():
6767+ log(f"sketch {args.name} already exists", mode="error")
6868+ return
6969+7070+ shutil.copy(template, new_sketch)
7171+ sketch_link = ac.file_link(new_sketch)
7272+ template_link = ac.file_link(template)
7373+7474+ log(f"created {sketch_link} from template {template_link}", mode="ok")
7575+7676+@_builtin("rrun")
7777+def _reload_run(session, args):
7878+ if session.try_handle_command("reload", ""):
7979+ session.try_handle_command("run", args)
8080+8181+@_builtin("run")
8282+def _run(session, args):
8383+ import os
8484+ import sys
8585+ import inspect
8686+ import time
8787+ import shutil
8888+ from pathlib import Path
8989+ from time import perf_counter
9090+ from importlib import import_module
9191+9292+ from pyt.core.sketch.run import run, handle_persistent
9393+ from pyt.core import lsnap
9494+9595+ sketch_name, remainder = lsnap(args)
9696+9797+ log = session.log
9898+9999+ try:
100100+ log(f"loading sketch \"{sketch_name}\"", mode="info")
101101+ module_name = f"{sketch_name}"
102102+ if module_name in sys.modules:
103103+ del sys.modules[module_name]
104104+ sys.path.insert(0, str(session.env.SKETCH))
105105+ sketch = import_module(module_name)
106106+ sys.path.pop(0)
107107+ except ModuleNotFoundError:
108108+ log.indented().trace()
109109+ log("no such sketch", mode="error", indent=4).blank()
110110+ return
111111+ except KeyboardInterrupt:
112112+ log("aborted", mode="info").blank()
113113+ return
114114+ except SystemExit:
115115+ raise
116116+ except:
117117+ log.indented().trace()
118118+ return
119119+120120+ sources = { name : inspect.getsource(member)
121121+ for name, member in inspect.getmembers(sketch)
122122+ if inspect.isfunction(member) and member.__module__ == module_name
123123+ }
124124+125125+ log = log.tag(sketch_name).mode("info")
126126+127127+ t0 = perf_counter()
128128+ if hasattr(sketch, "persistent"):
129129+ if not handle_persistent(session, sketch_name, sketch.persistent, sketch.__dict__, log, sources):
130130+ log.blank()
131131+ return
132132+133133+ sketch.__dict__.update(session.persistent_state)
134134+135135+ pyt_out = session.env.OUT
136136+137137+ if not pyt_out:
138138+ 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")
139139+ session.log("aborting.", mode="error")
140140+ return
141141+142142+ daily = time.strftime("%d.%m.%Y")
143143+ moment = time.strftime("t%H.%M.%S")
144144+145145+ run_dir = Path(os.path.join(pyt_out, sketch_name, daily, moment))
146146+ sketch.__dict__["run_dir"] = run_dir
147147+ run_dir.mkdir(parents=True, exist_ok=True)
148148+149149+ shutil.copy(sketch.__file__, run_dir / f"{sketch_name}.py")
150150+151151+ with open(run_dir / f".snakepyt", "w") as metadata:
152152+ metadata.write(f"snakepyt version {session.snakepyt_version[0]}.{session.snakepyt_version[1]}\n")
153153+154154+ if hasattr(sketch, "main"):
155155+ if hasattr(sketch, "final"):
156156+ finalizer = sketch.final.__code__
157157+ else:
158158+ finalizer = None
159159+ failures, runs = 0, 0
160160+ try:
161161+ failures, runs = run(session, sketch.main, None, (), sketch.__dict__, log, sources, finalizer)
162162+ except KeyboardInterrupt:
163163+ log.blank().log("aborted", mode="info").blank()
164164+ return
165165+ else:
166166+ log("sketch has no main function", mode="error", indent=4)
167167+168168+ if failures == 0:
169169+ log(f"finished {runs} run(s) in {perf_counter() - t0:.3f}s", mode="ok")
170170+ elif failures < runs:
171171+ log(f"finished {runs-failures} of {runs} run(s) in {perf_counter() - t0:.3f}s", mode="info")
172172+ log(f"{failures} run(s) failed to finish", mode="error")
173173+ else:
174174+ log(f"all {failures} run(s) failed to finish", mode="error")
175175+
+2
pyt/core/core.py
pyt/core/general.py
···2929 parts = s.lstrip().split(delimiter, 1)
3030 return (parts[0].rstrip(), parts[1].lstrip() if len(parts) > 1 else "")
31313232+from dataclasses import dataclass
3333+
+5-5
pyt/core/logger.py
pyt/core/terminal/logger.py
···99from typing import Optional
1010from pathlib import Path
11111212-from pyt.lib.ansi import codes as ac
1212+from pyt.core.terminal.ansi import codes as ac
13131414_tag_colors = {
1515 "error": ac.ansi(ac.fg("red")),
···104104 print()
105105 return self
106106107107- def input(self, username=None):
108108- if username is None:
109109- username = ":"
110110- return _input(f"{username}:", mode="user", indent=self._indent)
107107+ def input(self, fore):
108108+ if fore is None:
109109+ fore = ":"
110110+ return _input(f"{fore}", mode="user", indent=self._indent)
111111
-22
pyt/core/persona.py
···11-22-# TODO: weighted RNG; replace (list str) with (dict str->weight),
33-# precompute a single looping rng buffer w/ like, 101 entries of randoms in [0,1)
44-# normalize weights of each dict during startup
55-# iterator will take the next rng val, run thru the dict keys subtracting their value,
66-# stop when it hits / passes 0
77-88-_smiles = [":)", ":3", ":D", "c:", "^^", "^_^", "<3"]
99-_laughs = ["haha", "lol", "lmao", "hehe", "ha"]
1010-_hellos = ["hello", "hi", "hiya", "hey", "hiii"]
1111-1212-def _iterate(items):
1313- i = 0
1414- n = len(items)
1515- while True:
1616- i = (i + 1) % n
1717- yield items[i]
1818-1919-smiles = _iterate(_smiles)
2020-laughs = _iterate(_laughs)
2121-hellos = _iterate(_hellos)
2222-
+5-7
pyt/core/pywrapl.py
pyt/core/terminal/pywrapl.py
···1122import sys
3344-_VERSION = {
55- "major": [3],
66- "minor": [14],
77- "micro": [2]
88-}
44+_VERSION = [
55+ (3,14,2)
66+]
97108class VersionMismatch(RuntimeError):
119 pass
···2826 from _pyrepl import console, simple_interact, readline, trace, historical_reader
29273028 version = sys.version_info
3131- if not (version.major in _VERSION["major"] and version.minor in _VERSION["minor"] and version.micro in _VERSION["micro"]):
2929+ if not ((version.major, version.minor, version.micro) in _VERSION):
3230 if on_version_mismatch == "error":
3331 raise VersionMismatch("wrapper for unsupported _pyrepl module was designed around a different version")
3432 elif on_version_mismatch == "warning":
3533 log("wrapper for unsupported _pyrepl module was designed around a different version", mode="warning")
3636- elif on_version_mismatch == "ignore":
3434+ elif on_version_mismatch != "ignore":
3735 log("on_version_mismatch should be one of [\"error\", \"warning\", \"ignore\"]", mode="warning")
38363937 local["exit"] = _ReplExitSentinel()
+13-4
pyt/core/run.py
pyt/core/sketch/run.py
···66 return (schedule, _schedule)
7788def handle_persistent(session, sketch_name, persistent_fn, module_globals, log, sources):
99+ # TODO refactor to not parse the same shit more than once
1010+ import ast
911 import hashlib
1010- from pyt.core import try_dump_locals
1212+ import inspect
1313+ from pyt.core.sketch.sketch import try_dump_locals
1414+1515+ # strip semantically-irrelevant whitespace & comments
1616+ canonicalized_source = ast.unparse(ast.parse(sources[persistent_fn.__name__]))
1717+ bytecode_hash = hashlib.sha256(canonicalized_source.encode()).hexdigest()
11181212- # TODO better to hash the source code tbh
1313- bytecode_hash = hashlib.sha256(persistent_fn.__code__.co_code).hexdigest()
1419 if sketch_name in session.persistent_hashes:
1520 if bytecode_hash == session.persistent_hashes[sketch_name]:
1621 return True
2222+1723 (success, locals_or_err) = try_dump_locals(persistent_fn,
1824 sources[persistent_fn.__name__],
1925 [], {},
2026 module_globals, log)
2727+2128 if success:
2229 session.persistent_hashes[sketch_name] = bytecode_hash
3030+ # TODO: sketches should get their own persistent state dicts, w/ a non-default
3131+ # option to persist items into the top-level persistent_state instead
2332 session.persistent_state.update(locals_or_err)
2433 else:
2534 log(f"failed to run persistent function: {locals_or_err}", mode="error")
···3039def run(session, fn, arg, partial_id, outer_scope, log, sources, finalizer=None):
3140 from time import perf_counter
32413333- from pyt.core import try_dump_locals
4242+ from pyt.core.sketch.sketch import try_dump_locals
34433544 scope = dict(outer_scope)
3645 schedule, schedule_fn = establish_scheduler()
···11+22+from dataclasses import dataclass
33+44+class ColorRGB:
55+ pass
66+77+class Color256:
88+ pass
99+1010+class Color216:
1111+ pass
1212+1313+class ColorTerm:
1414+ pass
1515+1616+# TODO: come up with strong type for color and modes, and anything else that gets added here,
1717+# and once all that's done, we can make this a frozen dataclass
1818+class Style:
1919+ def __init__(self):
2020+ self.color = None
2121+ self.modes = [] # list of idk some kinda enum
2222+2323+class StyleChange:
2424+ pass
2525+2626+def _write(stream, text, style):
2727+ # convert style to ansi
2828+ # or if u wanna get real fancy, convert all *style changes* to ansi
2929+ pass
3030+3131+def _flush(stream):
3232+ pass
3333+3434+class _MockOut:
3535+ # TODO do-nothing methods for fake out stream
3636+ pass
3737+3838+class _MockIn:
3939+ # TODO do-nothing methods for fake in stream
4040+ pass
4141+4242+class Terminal:
4343+ """
4444+ Abstract terminal
4545+ """
4646+4747+ _in_stream = None
4848+ _out_streams = None
4949+5050+ def __init__(self, in_stream, out_streams):
5151+ self._in_stream = in_stream if in_stream else _MockIn()
5252+ if not out_streams or len(out_streams) == 0:
5353+ self._out_streams = { "default": _MockOut() }
5454+ else:
5555+ self._out_streams = out_streams
5656+ if "default" not in self._out_streams:
5757+ self._out_streams["default"] = next(iter(out_streams.values()))
5858+5959+ # TODO: figure out what kinds of styling each out stream actually supports
6060+ # including fancy kitty extension stuff! speaking of which, also check if we can
6161+ # get nice kitty-mode input events
6262+6363+ # construct style filters from this
6464+6565+ # TODO: function that turns (Style | None, Style | None) pair into StyleChange | None
6666+6767+ # TODO: function that turns an iterable of (text, Style | None) pairs
6868+ # into an iterable of text | StyleChange
6969+7070+ def write(self, text, style=None, to="default"):
7171+ target = self._out_streams.get(to, None)
7272+ if target is None:
7373+ raise RuntimeError("valid streams: [\"out\", \"err\"]")
7474+ if isinstance(text, list):
7575+ for entry in text:
7676+ _write(target, entry[0], entry[1])
7777+ else:
7878+ _write(target, text, style)
7979+ _flush(target)
8080+8181+ # TODO default link style
8282+ def write_link(self, uri, text, style=None, to="default"):
8383+ # need to handle the case where the link is made up of multiple styles ofc.
8484+ # just means tacking on a wrapper around text and style and passing them thru to
8585+ # write.
8686+ # also need to ensure link capability of the stream tho
8787+ pass
8888+8989+ def query(self, query, on_result, on_timeout=None):
9090+ # wrap ansi queries so that user doesn't have to poll
9191+ # maybe also expose a blocking version? idk
9292+ pass
9393+9494+ def read(self):
9595+ # just the text out of the input queue; kitty makes this easy but oldschool ansi
9696+ # code untangling might be a pain
9797+ pass
9898+9999+ def get_input_events(self, clear=True):
100100+ # access the input event queue. by default, consume it
101101+ # maybe add filtering for which kinds of events the caller cares about
102102+ pass
103103+