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