summaryrefslogtreecommitdiff
path: root/Biz
diff options
context:
space:
mode:
Diffstat (limited to 'Biz')
-rw-r--r--Biz/Bild/Example.py3
-rw-r--r--Biz/Mynion.py10
-rw-r--r--Biz/Repl.py168
3 files changed, 148 insertions, 33 deletions
diff --git a/Biz/Bild/Example.py b/Biz/Bild/Example.py
index 1bd30ae..dd9a6ef 100644
--- a/Biz/Bild/Example.py
+++ b/Biz/Bild/Example.py
@@ -6,9 +6,8 @@ Example Python file that also serves as a test case for bild.
# : out example
# : dep cryptography
-import sys
-
import cryptography.fernet
+import sys
def cryptic_hello(name: str) -> str:
diff --git a/Biz/Mynion.py b/Biz/Mynion.py
index 3b80f5f..83d427b 100644
--- a/Biz/Mynion.py
+++ b/Biz/Mynion.py
@@ -4,19 +4,17 @@
# : dep exllama
# : dep slixmpp
import argparse
+import Biz.Log
import dataclasses
+import exllama # type: ignore[import]
import logging
import os
import pathlib
-import sys
-import typing
-
-import exllama # type: ignore[import]
import slixmpp
import slixmpp.exceptions
+import sys
import torch
-
-import Biz.Log
+import typing
def smoosh(s: str) -> str:
diff --git a/Biz/Repl.py b/Biz/Repl.py
index 2f71c33..595b555 100644
--- a/Biz/Repl.py
+++ b/Biz/Repl.py
@@ -14,16 +14,22 @@ path to the same file. In the future this could be expanded to be a list of
additional files to load.
"""
+# : out python-improved-repl
+# : dep mypy
+import Biz.Log as Log
import importlib
import importlib.util
+import inspect
import logging
+import mypy.api
import os
+import pydoc
+import string
import subprocess
import sys
-
-import mypy.api
-
-from Biz import Log
+import textwrap
+import types
+import typing
def use(ns: str, path: str) -> None:
@@ -34,6 +40,9 @@ def use(ns: str, path: str) -> None:
"""
logging.info("loading %s from %s", ns, path)
spec = importlib.util.spec_from_file_location(ns, path)
+ if spec is None or spec.loader is None:
+ msg = "spec could not be loaded for %s at %s"
+ raise ValueError(msg, ns, path)
module = importlib.util.module_from_spec(spec)
# delete module and its imported names if its already loaded
if ns in sys.modules:
@@ -70,30 +79,139 @@ def edit_file(ns: str, path: str, editor: str) -> None:
try:
proc = subprocess.run([editor, path], check=False)
except FileNotFoundError:
- Log.fail("editor '%s' not found", editor)
+ logging.exception("editor '%s' not found", editor)
if proc.returncode == 0:
use(ns, path)
typecheck(path)
-if __name__ == "__main__":
- Log.setup()
- NS = sys.argv[1]
- PATH = sys.argv[2]
- EDITOR = os.environ.get("EDITOR", "$EDITOR")
- use(NS, PATH)
- typecheck(PATH)
-
- logging.info("use edit() to open %s in %s", NS, EDITOR)
- logging.info("use reload() after making changes")
- sys.ps1 = f"{NS}> "
- sys.ps2 = f"{NS}| "
-
- def reload() -> None:
- """Reload the namespace."""
- use(NS, PATH)
- typecheck(PATH)
-
- def edit() -> None:
+class CustomRepl:
+ """Custom repl commands, heavily inspired by ghci."""
+
+ def __init__(self, ns: str, path: str, editor: str) -> None:
+ """Create the custom repl for given ns and path."""
+ self.ns = ns
+ self.path = path
+ self.editor = editor
+ self.default = sys.__excepthook__
+ self.isframe = inspect.isframe
+ self.stack = inspect.stack
+ self.stdout = sys.stdout
+ self.whitespace = string.whitespace
+
+ def setup(self) -> None:
+ """
+ Load the NS, setup hooks and prompt.
+
+ This basically does all the heavy lifting of customizing the Python
+ repl.
+ """
+ # load given namespace
+ use(self.ns, self.path)
+ typecheck(self.path)
+ # setup hooks and prompt
+ sys.excepthook = self.excepthook
+ pydoc.pager = lambda text: pydoc.pipepager(text, "more")
+ sys.ps1 = f"{self.ns}> "
+ sys.ps2 = f"{self.ns}| "
+
+ def show_help(self) -> None:
+ """Print info about how to use this repl."""
+ sys.stdout.write(
+ textwrap.dedent(f"""
+ repl commands:
+ :e open {self.ns} in {self.editor}
+ :r reload {self.ns}
+ :t obj show the type of obj
+ obj? expands to 'help(obj)'
+ :? show this help
+ """),
+ )
+ sys.stdout.flush()
+
+ def excepthook(
+ self,
+ type_: type[BaseException],
+ value: BaseException,
+ traceback: types.TracebackType | None,
+ ) -> typing.Any:
+ """
+ Pre-process Python repl exceptions.
+
+ This is called on `sys.excepthook`, which runs when the repl doesn't
+ know how to handle some input. So, we inspect `value` and provide
+ alternate functionality, bottoming out at the default exception.
+ """
+ # ruff: noqa: PLR0911
+ if not isinstance(value, SyntaxError):
+ return self.default(type_, value, traceback)
+ if value.text is None:
+ msg = "value.text is None: %s"
+ raise ValueError(msg, value)
+ stmt = value.text.rstrip()
+ if stmt == ":?":
+ self.repl_help()
+ return None
+ if stmt.endswith("?"):
+ name = stmt.rstrip("?(" + self.whitespace)
+ self.wut(name)
+ return None
+ if stmt == ":e":
+ edit_file(self.ns, self.path, self.editor)
+ return None
+ if stmt == ":r":
+ use(self.ns, self.path)
+ typecheck(self.path)
+ return None
+ if stmt.startswith(":t"):
+ var = stmt.split()[1]
+ self.get_type(var)
+ return None
+ return self.default(type_, value, traceback)
+
+ def get_type(self, name: str) -> typing.Any | None:
+ """Return the type of `name` to the caller."""
+ for record in self.stack():
+ frame = record[0]
+ if not self.isframe(frame):
+ continue
+ cmd = f"typing.reveal_type({name})"
+ return eval(cmd, frame.f_globals, frame.f_locals) # noqa: S307
+ return None
+
+ def wut(self, name: str) -> typing.Any | None:
+ """Return the documentation for `name` to the caller."""
+ for record in self.stack():
+ frame = record[0]
+ if not self.isframe(frame):
+ continue
+ cmd = f"help({name})"
+ return eval(cmd, frame.f_globals, frame.f_locals) # noqa: S307
+ return None
+
+ def reload(self) -> None:
+ """Reload the current namespace."""
+ use(self.ns, self.path)
+ typecheck(self.path)
+
+ def edit(self) -> None:
"""Edit the current namespace."""
- edit_file(NS, PATH, EDITOR)
+ edit_file(self.ns, self.path, self.editor)
+
+
+def main() -> None:
+ """Entrypoint."""
+ if sys.argv[1] == "test":
+ pass
+ else:
+ Log.setup()
+ ns = sys.argv[1]
+ path = sys.argv[2]
+ editor = os.environ.get("EDITOR", "$EDITOR")
+ repl = CustomRepl(ns, path, editor)
+ repl.setup()
+ repl.show_help()
+
+
+if __name__ == "__main__":
+ main()