summaryrefslogtreecommitdiff
path: root/Biz/Repl.py
blob: 2f71c33b9d7919ea3cc13ee61cc84aea00db6fd7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""
Improve the standard Python REPL.

This module attempts to emulate the workflow of ghci or lisp repls. It uses
importlib to load a namespace from the provided path, typechecks it with mypy,
and provides some tools for improving repl-driven development.

This module is called in Biz/Ide/repl.sh like so:

    python -i Biz/Repl.py NS PATH

where NS is the dot-partitioned namespace of the main module, and PATH is the
path to the same file. In the future this could be expanded to be a list of
additional files to load.
"""

import importlib
import importlib.util
import logging
import os
import subprocess
import sys

import mypy.api

from Biz import Log


def use(ns: str, path: str) -> None:
    """
    Load or reload the module named 'ns' from 'path'.

    Like `use` in the Guile Scheme repl.
    """
    logging.info("loading %s from %s", ns, path)
    spec = importlib.util.spec_from_file_location(ns, path)
    module = importlib.util.module_from_spec(spec)
    # delete module and its imported names if its already loaded
    if ns in sys.modules:
        del sys.modules[ns]
        for name in module.__dict__:
            if name in globals():
                del globals()[name]
    sys.modules[ns] = module
    spec.loader.exec_module(module)
    names = list(module.__dict__)
    globals().update({k: getattr(module, k) for k in names})


def typecheck(path: str) -> None:
    """Typecheck this namespace."""
    # this envvar is undocumented, but it works
    # https://github.com/python/mypy/issues/13815
    os.environ["MYPY_FORCE_COLOR"] = "1"
    logging.info("typechecking %s", path)
    stdout, stderr, _ = mypy.api.run([path])
    sys.stdout.write(stdout)
    sys.stdout.flush()
    sys.stderr.write(stderr)
    sys.stderr.flush()


def edit_file(ns: str, path: str, editor: str) -> None:
    """
    Edit and reload the given namespace and path.

    It is assumed ns and path go together. If `editor` returns something other
    than 0, this function will not reload the ns.
    """
    try:
        proc = subprocess.run([editor, path], check=False)
    except FileNotFoundError:
        Log.fail("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:
        """Edit the current namespace."""
        edit_file(NS, PATH, EDITOR)