1"""Tools for helping manage xontributions."""
2import os
3import sys
4import json
5import builtins
6import argparse
7import functools
8import importlib
9import importlib.util
10
11from xonsh.tools import print_color, unthreadable
12
13
14@functools.lru_cache(1)
15def xontribs_json():
16    return os.path.join(os.path.dirname(__file__), "xontribs.json")
17
18
19def find_xontrib(name):
20    """Finds a xontribution from its name."""
21    if name.startswith("."):
22        spec = importlib.util.find_spec(name, package="xontrib")
23    else:
24        spec = importlib.util.find_spec("." + name, package="xontrib")
25    return spec or importlib.util.find_spec(name)
26
27
28def xontrib_context(name):
29    """Return a context dictionary for a xontrib of a given name."""
30    spec = find_xontrib(name)
31    if spec is None:
32        return None
33    m = importlib.import_module(spec.name)
34    pubnames = getattr(m, "__all__", None)
35    if pubnames is not None:
36        ctx = {k: getattr(m, k) for k in pubnames}
37    else:
38        ctx = {k: getattr(m, k) for k in dir(m) if not k.startswith("_")}
39    return ctx
40
41
42def prompt_xontrib_install(names):
43    """Returns a formatted string with name of xontrib package to prompt user"""
44    md = xontrib_metadata()
45    packages = []
46    for name in names:
47        for xontrib in md["xontribs"]:
48            if xontrib["name"] == name:
49                packages.append(xontrib["package"])
50
51    print(
52        "The following xontribs are enabled but not installed: \n"
53        "   {xontribs}\n"
54        "To install them run \n"
55        "    xpip install {packages}".format(
56            xontribs=" ".join(names), packages=" ".join(packages)
57        )
58    )
59
60
61def update_context(name, ctx=None):
62    """Updates a context in place from a xontrib. If ctx is not provided,
63    then __xonsh_ctx__ is updated.
64    """
65    if ctx is None:
66        ctx = builtins.__xonsh_ctx__
67    if not hasattr(update_context, "bad_imports"):
68        update_context.bad_imports = []
69    modctx = xontrib_context(name)
70    if modctx is None:
71        update_context.bad_imports.append(name)
72        return ctx
73    return ctx.update(modctx)
74
75
76@functools.lru_cache()
77def xontrib_metadata():
78    """Loads and returns the xontribs.json file."""
79    with open(xontribs_json(), "r") as f:
80        md = json.load(f)
81    return md
82
83
84def xontribs_load(names, verbose=False):
85    """Load xontribs from a list of names"""
86    ctx = builtins.__xonsh_ctx__
87    for name in names:
88        if verbose:
89            print("loading xontrib {0!r}".format(name))
90        update_context(name, ctx=ctx)
91    if update_context.bad_imports:
92        prompt_xontrib_install(update_context.bad_imports)
93        del update_context.bad_imports
94
95
96def _load(ns):
97    """load xontribs"""
98    xontribs_load(ns.names, verbose=ns.verbose)
99
100
101def _list(ns):
102    """Lists xontribs."""
103    meta = xontrib_metadata()
104    data = []
105    nname = 6  # ensures some buffer space.
106    names = None if len(ns.names) == 0 else set(ns.names)
107    for md in meta["xontribs"]:
108        name = md["name"]
109        if names is not None and md["name"] not in names:
110            continue
111        nname = max(nname, len(name))
112        spec = find_xontrib(name)
113        if spec is None:
114            installed = loaded = False
115        else:
116            installed = True
117            loaded = spec.name in sys.modules
118        d = {"name": name, "installed": installed, "loaded": loaded}
119        data.append(d)
120    if ns.json:
121        jdata = {d.pop("name"): d for d in data}
122        s = json.dumps(jdata)
123        print(s)
124    else:
125        s = ""
126        for d in data:
127            name = d["name"]
128            lname = len(name)
129            s += "{PURPLE}" + name + "{NO_COLOR}  " + " " * (nname - lname)
130            if d["installed"]:
131                s += "{GREEN}installed{NO_COLOR}      "
132            else:
133                s += "{RED}not-installed{NO_COLOR}  "
134            if d["loaded"]:
135                s += "{GREEN}loaded{NO_COLOR}"
136            else:
137                s += "{RED}not-loaded{NO_COLOR}"
138            s += "\n"
139        print_color(s[:-1])
140
141
142@functools.lru_cache()
143def _create_xontrib_parser():
144    # parse command line args
145    parser = argparse.ArgumentParser(
146        prog="xontrib", description="Manages xonsh extensions"
147    )
148    subp = parser.add_subparsers(title="action", dest="action")
149    load = subp.add_parser("load", help="loads xontribs")
150    load.add_argument(
151        "-v", "--verbose", action="store_true", default=False, dest="verbose"
152    )
153    load.add_argument("names", nargs="+", default=(), help="names of xontribs")
154    lyst = subp.add_parser(
155        "list", help=("list xontribs, whether they are " "installed, and loaded.")
156    )
157    lyst.add_argument(
158        "--json", action="store_true", default=False, help="reports results as json"
159    )
160    lyst.add_argument("names", nargs="*", default=(), help="names of xontribs")
161    return parser
162
163
164_MAIN_XONTRIB_ACTIONS = {"load": _load, "list": _list}
165
166
167@unthreadable
168def xontribs_main(args=None, stdin=None):
169    """Alias that loads xontribs"""
170    if not args or (
171        args[0] not in _MAIN_XONTRIB_ACTIONS and args[0] not in {"-h", "--help"}
172    ):
173        args.insert(0, "load")
174    parser = _create_xontrib_parser()
175    ns = parser.parse_args(args)
176    if ns.action is None:  # apply default action
177        ns = parser.parse_args(["load"] + args)
178    return _MAIN_XONTRIB_ACTIONS[ns.action](ns)
179