From 1b2ec11c2f4d5932ee0757aa1ec72e0730ed5425 Mon Sep 17 00:00:00 2001 From: daylinmorgan Date: Mon, 29 May 2023 18:13:10 +0000 Subject: [PATCH] deploy: b5b02f82a60d3a1e7cba0653075da29cbe2fbd33 --- viv.py | 137 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 33 deletions(-) diff --git a/viv.py b/viv.py index 0db7483..c374fc9 100755 --- a/viv.py +++ b/viv.py @@ -52,7 +52,7 @@ from typing import ( from urllib.error import HTTPError from urllib.request import urlopen -__version__ = "23.5a2-2-gebb657c-dev" +__version__ = "23.5a3-10-gf2c156d-dev" class Config: @@ -226,7 +226,8 @@ class Ansi: Args: output: text output from subprocess, usually from p.stdout """ - + if not output: + return echo("subprocess output:") new_output = [f"{self.red}->{self.end} {line}" for line in output.splitlines()] sys.stdout.write("\n".join(new_output) + "\n") @@ -559,11 +560,12 @@ class ViVenv: name: str = "", path: Path | None = None, ) -> None: - self.spec = spec + self.spec = self._validate_spec(spec) self.exe = str(Path(sys.executable).resolve()) if track_exe else "N/A" self.id = id if id else get_hash(spec, track_exe) self.name = name if name else self.id self.path = path if path else c.venvcache / self.name + self.exists = self.name in [d.name for d in c.venvcache.iterdir()] @classmethod def load(cls, name: str) -> "ViVenv": @@ -583,6 +585,18 @@ class ViVenv: return vivenv + def _validate_spec(self, spec: List[str]) -> List[str]: + """ensure spec is at least of sequence of strings + + Args: + spec: sequence of package specifications + """ + if not set(map(type, spec)) == {str}: + error("unexepected input in package spec") + error(f"check your packages definitions: {spec}", code=1) + + return sorted(spec) + def create(self, quiet: bool = False) -> None: if not quiet: echo(f"new unique vivenv -> {self.name}") @@ -636,12 +650,9 @@ def use(*packages: str, track_exe: bool = False, name: str = "") -> Path: track_exe: if true make env python exe specific name: use as vivenv name, if not provided id is used """ - validate_spec(packages) vivenv = ViVenv(list(packages), track_exe=track_exe, name=name) - if vivenv.name not in [d.name for d in c.venvcache.iterdir()] or os.getenv( - "VIV_FORCE" - ): + if not vivenv.exists or os.getenv("VIV_FORCE"): vivenv.create() vivenv.install_pkgs() vivenv.dump_info(write=True) @@ -650,18 +661,6 @@ def use(*packages: str, track_exe: bool = False, name: str = "") -> Path: return vivenv.path -def validate_spec(spec: Tuple[str, ...]) -> None: - """ensure spec is at least of sequence of strings - - Args: - spec: sequence of package specifications - """ - # ? make this a part of ViVenv? - if not set(map(type, spec)) == {str}: - error("unexepected input in package spec") - error(f"check your packages definitions: {spec}", code=1) - - def modify_sys_path(new_path: Path) -> None: # remove user-site for i, path in enumerate(sys.path): @@ -1025,7 +1024,7 @@ class Viv: f"{pip_path} {' '.join(args.cmd)}" if args.exe == "pip" else f"{python_path} {' '.join(args.cmd)}" - ) + ) + " ".join(args.rest) echo(f"executing {cmd}") run(shlex.split(cmd), verbose=True) @@ -1048,16 +1047,19 @@ class Viv: make_executable(src) echo("symlinking cli") - if not cli.is_file(): + if cli.is_file() and confirm( + f"Existing file at {a.style(str(cli),'bold')}, " + "would you like to overwrite it?" + ): + cli.unlink() cli.symlink_to(src) else: - cli.unlink() cli.symlink_to(src) echo("Remember to include the following line in your shell rc file:") sys.stderr.write( ' export PYTHONPATH="$PYTHONPATH:$HOME/' - f'{src.relative_to(Path.home())}"\n' + f'{src.relative_to(Path.home()).parent}"\n' ) def manage(self, args: Namespace) -> None: @@ -1179,6 +1181,10 @@ class Viv: "`python3 <(curl -fsSL gh.dayl.in/viv/viv.py) manage install`" ) + def _pick_bin(self, args: Namespace) -> Tuple[str, str]: + default = re.split(r"[=><~!*]+", args.reqs[0])[0] + return default, (default if not args.bin else args.bin) + def shim(self, args: Namespace) -> None: """\ generate viv-powered cli apps @@ -1192,8 +1198,7 @@ class Viv: if not args.reqs: error("please specify at lease one dependency", code=1) - default_bin = re.split(r"[=><~!*]+", args.reqs[0])[0] - bin = default_bin if not args.bin else args.bin + default_bin, bin = self._pick_bin(args) output = ( c.binparent / default_bin if not args.output else args.output.absolute() ) @@ -1240,6 +1245,43 @@ class Viv: make_executable(output) + def run(self, args: Namespace) -> None: + """\ + run an app w/ an on-demand venv + + examples: + viv r pycowsay -- "Viv isn't venv\!" + viv r rich -b python -- -m rich + """ + if not args.reqs: + error("please specify at lease one dependency", code=1) + + _, bin = self._pick_bin(args) + spec = combined_spec(args.reqs, args.requirements) + vivenv = ViVenv(spec) + + # TODO: respect a VIV_RUN_MODE env variable as the same as keep i.e. + # ephemeral (default), semi-ephemeral (persist inside /tmp), or + # persist (use c.cache) + + if not vivenv.exists or os.getenv("VIV_FORCE"): + if not args.keep: + with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir: + vivenv.path = Path(tmpdir) + vivenv.create() + vivenv.install_pkgs() + sys.exit( + subprocess.run( + [vivenv.path / "bin" / bin, *args.rest] + ).returncode + ) + else: + vivenv.create() + vivenv.install_pkgs() + vivenv.dump_info(write=True) + + sys.exit(subprocess.run([vivenv.path / "bin" / bin, *args.rest]).returncode) + def _get_subcmd_parser( self, subparsers: _SubParsersAction[ArgumentParser], @@ -1409,11 +1451,10 @@ class Viv: "purge", help="remove traces of viv", aliases="p", parents=[p_manage_shared] ).set_defaults(func=self.manage, cmd="purge") - ( - p_shim := self._get_subcmd_parser( - subparsers, "shim", parents=[p_freeze_shim_shared] - ) - ).set_defaults(func=self.shim, cmd="shim") + p_shim = self._get_subcmd_parser( + subparsers, "shim", parents=[p_freeze_shim_shared] + ) + p_shim.add_argument( "-f", "--freeze", @@ -1427,11 +1468,41 @@ class Viv: type=Path, metavar="", ) - p_shim.add_argument("-b", "--bin", help="console_script/script to invoke") + p_shim.add_argument( + "-b", "--bin", help="console_script/script to invoke", metavar="" + ) - args = parser.parse_args() + p_run = self._get_subcmd_parser(subparsers, "run") - args.func(args) + p_run.add_argument( + "-r", + "--requirements", + help="path/to/requirements.txt file", + metavar="", + ) + p_run.add_argument( + "-k", + "--keep", + help="preserve environment", + action="store_true", + ) + p_run.add_argument("reqs", help="requirements specifiers", nargs="*") + + p_run.add_argument( + "-b", "--bin", help="console_script/script to invoke", metavar="" + ) + + if "--" in sys.argv: + i = sys.argv.index("--") + args = parser.parse_args(sys.argv[1:i]) + args.rest = sys.argv[i + 1 :] + else: + args = parser.parse_args() + args.rest = [] + + args.func( + args, + ) def main() -> None: