summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Biz/Bild/Example.py3
-rw-r--r--Biz/Mynion.py10
-rw-r--r--Biz/Repl.py168
-rw-r--r--pyproject.toml33
4 files changed, 169 insertions, 45 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()
diff --git a/pyproject.toml b/pyproject.toml
index 62eebf4..025e630 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,6 +7,7 @@ exclude = ["_", ".git"]
line-length = 80
indent-width = 4
target-version = "py310"
+unsafe-fixes = true
[tool.ruff.format]
preview = true
@@ -20,16 +21,24 @@ preview = true
select = ["ALL"]
fixable = ["ALL"]
ignore = [
- "ANN401", # any-type, we allow typing.Any, although we shouldn't
- "CPY001", # missing-copyright-notice
- "D203", # no-blank-line-before-class
- "D212", # multi-line-summary-first-line
- "E203", # whitespace-before-punctuation, doesn't work with ruff format
- "INP001", # implicit-namespace-package
- "N999", # invalid-module-name
- "PT009", # pytest-unittest-assertion, conflicts with assert (S101)
- "S310", # suspicious-url-open-usage, doesn't work in 0.1.5
- "S404", # suspicious-subprocess-import, not stable
- "S603", # subprocess-without-shell-equals-true, false positives
- "S607", # start-process-with-partial-path
+ "ANN401", # any-type, we allow typing.Any, although we shouldn't
+ "CPY001", # missing-copyright-notice
+ "D203", # no-blank-line-before-class
+ "D212", # multi-line-summary-first-line
+ "E203", # whitespace-before-punctuation, doesn't work with ruff format
+ "INP001", # implicit-namespace-package
+ "N999", # invalid-module-name
+ "PLR0402", # manual-from-import, prefer imports like Haskell
+ "PT009", # pytest-unittest-assertion, conflicts with assert (S101)
+ "S310", # suspicious-url-open-usage, doesn't work in 0.1.5
+ "S404", # suspicious-subprocess-import, not stable
+ "S603", # subprocess-without-shell-equals-true, false positives
+ "S607", # start-process-with-partial-path
+ "TD002", # missing-todo-author, just don't allow todos
+ "TD003", # missing-todo-link, just don't allow todos
]
+
+[tool.ruff.lint.isort]
+no-sections = true
+force-single-line = true
+