diff --git a/src/viv/viv.py b/src/viv/viv.py index 3c8fc96..c77f132 100755 --- a/src/viv/viv.py +++ b/src/viv/viv.py @@ -21,6 +21,8 @@ import sys import tempfile import threading import time +from urllib.request import urlopen +from urllib.error import HTTPError import venv from argparse import SUPPRESS, Action from argparse import ArgumentParser as StdArgParser @@ -49,7 +51,7 @@ from typing import ( Generator, ) -__version__ = "22.12a3-41-g5c40210-dev" +__version__ = "22.12a3-43-g0e9779a-dev" @dataclass @@ -305,6 +307,16 @@ def echo( fd.write(output) +def confirm(question: str) -> bool: + while True: + ans = input(question + a.style(" (Y)es/(n)o: ", "yellow")).strip().lower() + if ans in ("y", "yes"): + return True + elif ans in ("n", "no"): + return False + sys.stdout.write("\nPlease select (Y)es or (n)o.") + + def run( command: List[str], spinmsg: str = "", @@ -448,7 +460,7 @@ class ViVenv: a.table((("key", "value"), *((k, v) for k, v in info.items()))) -def use(*packages: str, track_exe: bool = False, name: str = "") -> None: +def use(*packages: str, track_exe: bool = False, name: str = "") -> ViVenv: """create a vivenv and append to sys.path Args: @@ -467,6 +479,7 @@ def use(*packages: str, track_exe: bool = False, name: str = "") -> None: vivenv.dump_info(write=True) modify_sys_path(vivenv.path) + return vivenv def validate_spec(spec: Tuple[str, ...]) -> None: @@ -515,8 +528,7 @@ STANDALONE_TEMPLATE = r""" # >>>>> code golfed with <3 """ # noqa -STANDALONE_TEMPLATE_USE = r""" -def _viv_use(*pkgs: str, track_exe: bool = False, name: str = "") -> None: +STANDALONE_TEMPLATE_USE = r"""def _viv_use(*pkgs: str, track_exe: bool = False, name: str = "") -> None: i,s,m,e,spec=__import__,str,map,lambda x: True if x else False,[*pkgs] if not {{*m(type,pkgs)}}=={{s}}: raise ValueError(f"spec: {{pkgs}} is invalid") ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write @@ -535,9 +547,13 @@ def _viv_use(*pkgs: str, track_exe: bool = False, name: str = "") -> None: i("json").dump({{"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe}},f) sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] _viv_use({spec}) -"""[ # noqa - 1: -] +""" # noqa + +SHOW_TEMPLATE = f""" + {a.style('Version', 'bold')}: {{version}} + {a.style('CLI', 'bold')}: {{cli}} + {a.style('Current Source', 'bold')}: {{src}} +""" def noqa(txt: str) -> str: @@ -691,7 +707,7 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter): # determine the required width and the entry label help_position = min(self._action_max_length + 2, self._max_help_position) help_width = max(self._width - help_position, 11) - action_width = help_position - self._current_indent - 2 + action_width = help_position - self._current_indent action_header = self._format_action_invocation(action) action_header_len = len(a.escape(action_header)) @@ -763,7 +779,8 @@ class ArgumentParser(StdArgParser): super().__init__(*args, **kwargs) self.formatter_class = lambda prog: CustomHelpFormatter( - prog, max_help_position=35 + prog, + max_help_position=35, ) def error(self, message: str) -> NoReturn: @@ -788,6 +805,12 @@ within python script: class Viv: def __init__(self) -> None: self.vivenvs = get_venvs() + self.current_source = Path(__file__).resolve() + self.name = ( + "python3 <(curl -fsSL gh.dayl.in/viv/viv.py)" + if str(self.current_source).startswith("/proc/") + else "viv" + ) def _match_vivenv(self, name_id: str) -> ViVenv: # type: ignore[return] # TODO: improve matching algorithm to favor names over id's @@ -895,11 +918,81 @@ class Viv: vivenv.dump_info() + def manage(self, args: Namespace) -> None: + """manage viv installation""" + + if args.cmd == "show": + # NOTE: could reuse the table output for this? + echo("Current:") + sys.stdout.write( + SHOW_TEMPLATE.format( + version=__version__, + cli=shutil.which("viv"), + src=Path(__file__).resolve(), + ) + ) + + elif args.cmd == "update": + if str(Path(__file__).resolve()).startswith("/proc/"): + error( + a.style("viv manage update", "bold") + + " should only be used with a locally installed viv", + 1, + ) + try: + r = urlopen( + "https://raw.githubusercontent.com/daylinmorgan/viv/" + + args.reference + + "/src/viv/viv.py" + ) + except HTTPError as e: + error( + "Issue updating viv see below:" + + a.style("-> ", "red").join(["\n"] + repr(e).splitlines()) + ) + if "404" in repr(e): + echo("Please check your reference is valid.", style="red") + sys.exit(1) + + viv_src = r.read() + + (hash := hashlib.sha256()).update(viv_src) + sha256 = hash.hexdigest() + + ( + src_cache := Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) + / "viv" + / "src" + ).mkdir(exist_ok=True, parents=True) + cached_version = src_cache / f"{sha256}.py" + + if not cached_version.is_file(): + with cached_version.open("w") as f: + f.write(viv_src.decode()) + + sys.path.append(str(src_cache)) + next_version = __import__(sha256).__version__ + q = ( + "Update source at: " + + a.style(Path(__file__).resolve(), "bold") + + f" \n from version {__version__} to {next_version}?" + ) + + if confirm(q): + print("AWAY THEN") + + elif args.cmd == "install": + echo("not yet implemented. sorry") + def _get_subcmd_parser( - self, subparsers: _SubParsersAction[ArgumentParser], name: str, **kwargs: Any + self, + subparsers: _SubParsersAction[ArgumentParser], + name: str, + attr: Optional[str] = None, + **kwargs: Any, ) -> ArgumentParser: aliases = kwargs.pop("aliases", [name[0]]) - cmd = getattr(self, name) + cmd = getattr(self, attr if attr else name) parser: ArgumentParser = subparsers.add_parser( name, help=cmd.__doc__.splitlines()[0], @@ -914,7 +1007,7 @@ class Viv: def cli(self) -> None: """cli entrypoint""" - parser = ArgumentParser(description=description) + parser = ArgumentParser(prog=self.name, description=description) parser.add_argument( "-V", "--version", @@ -927,10 +1020,7 @@ class Viv: ) p_vivenv_arg = ArgumentParser(add_help=False) p_vivenv_arg.add_argument("vivenv", help="name/hash of vivenv") - p_list = self._get_subcmd_parser( - subparsers, - "list", - ) + p_list = self._get_subcmd_parser(subparsers, "list") p_list.add_argument( "-q", @@ -955,16 +1045,15 @@ class Viv: nargs="*", ) - p_exe_python = p_exe_sub.add_parser( + p_exe_sub.add_parser( "python", help="run command with python", parents=[p_vivenv_arg, p_exe_shared], - ) - p_exe_pip = p_exe_sub.add_parser( + ).set_defaults(func=self.exe, exe="python") + + p_exe_sub.add_parser( "pip", help="run command with pip", parents=[p_vivenv_arg, p_exe_shared] - ) - p_exe_python.set_defaults(func=self.exe, exe="python") - p_exe_pip.set_defaults(func=self.exe, exe="pip") + ).set_defaults(func=self.exe, exe="pip") p_remove = self._get_subcmd_parser( subparsers, @@ -1009,6 +1098,31 @@ class Viv: parents=[p_vivenv_arg], ) + p_manage_sub = self._get_subcmd_parser( + subparsers, name="manage" + ).add_subparsers(title="subcommand", metavar="", required=True) + + p_manage_sub.add_parser( + "install", help="install viv", aliases="i" + ).set_defaults(func=self.manage, cmd="install") + + ( + p_manage_update := p_manage_sub.add_parser( + "update", help="update viv version", aliases="u" + ) + ).set_defaults(func=self.manage, cmd="update") + + p_manage_update.add_argument( + "-r", + "--reference", + help="git reference (branch/tag/commit)", + default="main", + ) + + p_manage_sub.add_parser( + "show", help="show current installation info", aliases="s" + ).set_defaults(func=self.manage, cmd="show") + args = parser.parse_args() args.func(args)