refactor: use named args/types

This commit is contained in:
Daylin Morgan 2023-06-05 09:08:45 -05:00
parent fc7a77175c
commit 9b533c9c32
Signed by: daylin
GPG key ID: C1E52E7DD81DF79F

View file

@ -50,7 +50,7 @@ from typing import (
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.request import urlopen from urllib.request import urlopen
__version__ = "23.5a5-7-g6bf9a91-dev" __version__ = "23.5a5-10-g5326324-dev"
class Spinner: class Spinner:
@ -117,7 +117,7 @@ class Env:
xdg_data_home=Path.home() / ".local" / "share", xdg_data_home=Path.home() / ".local" / "share",
) )
def __getattr__(self, attr): def __getattr__(self, attr: str) -> Any:
if not attr.startswith("_") and (defined := getattr(self, f"_{attr}")): if not attr.startswith("_") and (defined := getattr(self, f"_{attr}")):
return defined return defined
else: else:
@ -129,11 +129,11 @@ class Env:
@property @property
def _viv_spec(self) -> List[str]: def _viv_spec(self) -> List[str]:
return filter(None, os.getenv("VIV_SPEC", "").split(" ")) return [i for i in os.getenv("VIV_SPEC", "").split(" ") if i]
class Cache: class Cache:
def __init__(self): def __init__(self) -> None:
self.base = Env().viv_cache self.base = Env().viv_cache
@staticmethod @staticmethod
@ -153,7 +153,9 @@ class Cache:
class Cfg: class Cfg:
@property @property
def src(self) -> Path: def src(self) -> Path:
return Path(Env().xdg_data_home) / "viv" / "viv.py" p = Path(Env().xdg_data_home) / "viv" / "viv.py"
p.parent.mkdir(exist_ok=True, parents=True)
return p
class Ansi: class Ansi:
@ -856,8 +858,8 @@ def combined_spec(reqs: List[str], requirements: Path) -> List[str]:
return reqs return reqs
def resolve_deps(args: Namespace) -> List[str]: def resolve_deps(reqs: List[str], requirements: Path) -> List[str]:
spec = combined_spec(args.reqs, args.requirements) spec = combined_spec(reqs, requirements)
cmd = [ cmd = [
"pip", "pip",
@ -918,7 +920,8 @@ def make_executable(path: Path) -> None:
def uses_viv(txt: str) -> bool: def uses_viv(txt: str) -> bool:
return re.search( return bool(
re.search(
""" """
\s*__import__\(\s*["']viv["']\s*\).use\(.* \s*__import__\(\s*["']viv["']\s*\).use\(.*
| |
@ -929,6 +932,7 @@ def uses_viv(txt: str) -> bool:
txt, txt,
re.VERBOSE, re.VERBOSE,
) )
)
class Viv: class Viv:
@ -979,7 +983,7 @@ class Viv:
else: else:
error(f"no matches found for {name_id}", code=1) error(f"no matches found for {name_id}", code=1)
def remove(self, args: Namespace) -> None: def remove(self, vivenvs: List[str]) -> None:
"""\ """\
remove a vivenv remove a vivenv
@ -987,7 +991,7 @@ class Viv:
`viv rm $(viv l -q)` `viv rm $(viv l -q)`
""" """
for name in args.vivenv: for name in vivenvs:
vivenv = self._match_vivenv(name) vivenv = self._match_vivenv(name)
if vivenv.path.is_dir(): if vivenv.path.is_dir():
echo(f"removing {vivenv.name}") echo(f"removing {vivenv.name}")
@ -998,11 +1002,19 @@ class Viv:
code=1, code=1,
) )
def freeze(self, args: Namespace) -> None: def freeze(
self,
reqs: List[str],
requirements: Path,
keep: bool,
standalone: bool,
path: str,
args: Namespace,
) -> None:
"""create import statement from package spec""" """create import statement from package spec"""
spec = resolve_deps(args) spec = resolve_deps(reqs, requirements)
if args.keep: if keep:
# re-create env again since path's are hard-coded # re-create env again since path's are hard-coded
vivenv = ViVenv(spec) vivenv = ViVenv(spec)
@ -1018,26 +1030,26 @@ class Viv:
echo("see below for import statements\n") echo("see below for import statements\n")
if args.standalone: if standalone:
sys.stdout.write(t.standalone(spec)) sys.stdout.write(t.standalone(spec))
return return
if args.path and not self.local_source: if path and not self.local_source:
error("No local viv found to import from", code=1) error("No local viv found to import from", code=1)
sys.stdout.write(t.frozen_import(args.path, self.local_source, spec)) sys.stdout.write(t.frozen_import(path, self.local_source, spec))
def list(self, args: Namespace) -> None: def list(self, quiet: bool, full: bool, use_json: bool) -> None:
"""list all vivenvs""" """list all vivenvs"""
if args.quiet: if quiet:
sys.stdout.write("\n".join(self.vivenvs) + "\n") sys.stdout.write("\n".join(self.vivenvs) + "\n")
elif len(self.vivenvs) == 0: elif len(self.vivenvs) == 0:
echo("no vivenvs setup") echo("no vivenvs setup")
elif args.full: elif full:
for _, vivenv in self.vivenvs.items(): for _, vivenv in self.vivenvs.items():
vivenv.tree() vivenv.tree()
elif args.json: elif use_json:
sys.stdout.write( sys.stdout.write(
json.dumps({k: v.meta.__dict__ for k, v in self.vivenvs.items()}) json.dumps({k: v.meta.__dict__ for k, v in self.vivenvs.items()})
) )
@ -1045,7 +1057,7 @@ class Viv:
for _, vivenv in self.vivenvs.items(): for _, vivenv in self.vivenvs.items():
vivenv.show() vivenv.show()
def exe(self, args: Namespace) -> None: def exe(self, vivenv_id: str, cmd: str, rest: List[str]) -> None:
"""\ """\
run binary/script in existing vivenv run binary/script in existing vivenv
@ -1054,31 +1066,31 @@ class Viv:
viv exe <vivenv> python -- script.py viv exe <vivenv> python -- script.py
""" """
vivenv = self._match_vivenv(args.vivenv) vivenv = self._match_vivenv(vivenv_id)
bin = vivenv.path / "bin" / args.cmd bin = vivenv.path / "bin" / cmd
if not bin.exists(): if not bin.exists():
error(f"{args.cmd} does not exist in {vivenv.name}", code=1) error(f"{cmd} does not exist in {vivenv.name}", code=1)
cmd = [bin, *args.rest] full_cmd = [str(bin), *rest]
run(cmd, verbose=True) run(full_cmd, verbose=True)
def info(self, args: Namespace) -> None: def info(self, vivenv_id: str, use_json: bool) -> None:
"""get metadata about a vivenv""" """get metadata about a vivenv"""
vivenv = self._match_vivenv(args.vivenv) vivenv = self._match_vivenv(vivenv_id)
metadata_file = vivenv.path / "vivmeta.json" metadata_file = vivenv.path / "vivmeta.json"
if not metadata_file.is_file(): if not metadata_file.is_file():
error(f"Unable to find metadata for vivenv: {args.vivenv}", code=1) error(f"Unable to find metadata for vivenv: {vivenv_id}", code=1)
if args.json: if use_json:
sys.stdout.write(json.dumps(vivenv.meta.__dict__)) sys.stdout.write(json.dumps(vivenv.meta.__dict__))
else: else:
vivenv.tree() vivenv.tree()
def _install_local_src(self, sha256: str, src: Path, cli: Path, yes: bool) -> None: def _install_local_src(self, sha256: str, src: Path, cli: Path, yes: bool) -> None:
echo("updating local source copy of viv") echo("updating local source copy of viv")
shutil.copy(Cache.src / f"{sha256}.py", src) shutil.copy(Cache().src / f"{sha256}.py", src)
make_executable(src) make_executable(src)
echo("symlinking cli") echo("symlinking cli")
@ -1097,14 +1109,18 @@ class Viv:
) )
def _get_new_version(self, ref: str) -> Tuple[str, str]: def _get_new_version(self, ref: str) -> Tuple[str, str]:
sys.path.append(str(Cache.src)) sys.path.append(str(Cache().src))
return (sha256 := fetch_source(ref)), __import__(sha256).__version__ return (sha256 := fetch_source(ref)), __import__(sha256).__version__
def manage(self, args: Namespace) -> None: def manage(self) -> None:
"""manage viv itself""" """manage viv itself"""
if args.subcmd == "show": def manage_show(
if args.pythonpath: self,
pythonpath: bool = False,
) -> None:
"""manage viv itself"""
if pythonpath:
if self.local and self.local_source: if self.local and self.local_source:
sys.stdout.write(str(self.local_source.parent) + "\n") sys.stdout.write(str(self.local_source.parent) + "\n")
else: else:
@ -1119,29 +1135,41 @@ class Viv:
) )
) )
elif args.subcmd == "update": def manage_update(
sha256, next_version = self._get_new_version(args.ref) self,
ref: str,
src: Path,
cli: Path,
yes: bool,
) -> None:
sha256, next_version = self._get_new_version(ref)
if self.local_version == next_version: if self.local_version == next_version:
echo(f"no change between {args.ref} and local version") echo(f"no change between {ref} and local version")
sys.exit(0) sys.exit(0)
if confirm( if confirm(
"Would you like to perform the above installation steps?", "Would you like to perform the above installation steps?",
t.update(self.local_source, args.cli, self.local_version, next_version), t.update(self.local_source, cli, self.local_version, next_version),
yes=args.yes, yes=yes,
): ):
self._install_local_src( self._install_local_src(
sha256, sha256,
Path( Path(
args.src if not self.local_source else self.local_source, src if not self.local_source else self.local_source,
), ),
args.cli, cli,
args.yes, yes,
) )
elif args.subcmd == "install": def manage_install(
sha256, downloaded_version = self._get_new_version(args.ref) self,
ref: str,
src: Path,
cli: Path,
yes: bool,
) -> None:
sha256, downloaded_version = self._get_new_version(ref)
echo(f"Downloaded version: {downloaded_version}") echo(f"Downloaded version: {downloaded_version}")
@ -1150,33 +1178,37 @@ class Viv:
if confirm( if confirm(
"Would you like to perform the above installation steps?", "Would you like to perform the above installation steps?",
t.install(args.src, args.cli), t.install(src, cli),
yes=args.yes, yes=yes,
): ):
self._install_local_src(sha256, args.src, args.cli) self._install_local_src(sha256, src, cli, yes)
elif args.subcmd == "purge": def manage_purge(
self,
ref: str,
src: Path,
cli: Path,
yes: bool,
) -> None:
to_remove = [] to_remove = []
if Cache.base.is_dir(): if Cache().base.is_dir():
to_remove.append(Cache.base) to_remove.append(Cache().base)
if args.src.is_file(): if src.is_file():
to_remove.append( to_remove.append(src.parent if src == (Cfg().src) else src)
args.src.parent if args.src == (Cfg().src) else args.src
)
if self.local_source and self.local_source.is_file(): if self.local_source and self.local_source.is_file():
if self.local_source.parent.name == "viv": if self.local_source.parent.name == "viv":
to_remove.append(self.local_source.parent) to_remove.append(self.local_source.parent)
else: else:
to_remove.append(self.local_source) to_remove.append(self.local_source)
if args.cli.is_file(): if cli.is_file():
to_remove.append(args.cli) to_remove.append(cli)
to_remove = list(set(to_remove)) to_remove = list(set(to_remove))
if confirm( if confirm(
"Remove the above files/directories?", "Remove the above files/directories?",
"\n".join(f" - {a.red}{p}{a.end}" for p in to_remove) + "\n", "\n".join(f" - {a.red}{p}{a.end}" for p in to_remove) + "\n",
yes=args.yes, yes=yes,
): ):
for p in to_remove: for p in to_remove:
if p.is_dir(): if p.is_dir():
@ -1189,11 +1221,21 @@ class Viv:
"`python3 <(curl -fsSL viv.dayl.in/viv.py) manage install`" "`python3 <(curl -fsSL viv.dayl.in/viv.py) manage install`"
) )
def _pick_bin(self, args: Namespace) -> Tuple[str, str]: def _pick_bin(self, reqs: List[str], bin: str) -> Tuple[str, str]:
default = re.split(r"[=><~!*]+", args.reqs[0])[0] default = re.split(r"[=><~!*]+", reqs[0])[0]
return default, (default if not args.bin else args.bin) return default, (default if not bin else bin)
def shim(self, args: Namespace) -> None: def shim(
self,
reqs: List[str],
requirements: Path,
bin: str,
output: Path,
freeze: bool,
yes: bool,
path: str,
standalone: bool,
) -> None:
"""\ """\
generate viv-powered cli apps generate viv-powered cli apps
@ -1201,33 +1243,35 @@ class Viv:
viv shim black viv shim black
viv shim yartsu -o ~/bin/yartsu --standalone viv shim yartsu -o ~/bin/yartsu --standalone
""" """
default_bin, bin = self._pick_bin(args) default_bin, bin = self._pick_bin(reqs, bin)
output = ( output = Env().viv_bin_dir / default_bin if not output else output.absolute()
Env().viv_bin_dir / default_bin
if not args.output
else args.output.absolute()
)
if output.is_file(): if output.is_file():
error(f"{output} already exists...exiting", code=1) error(f"{output} already exists...exiting", code=1)
if args.freeze: if freeze:
spec = resolve_deps(args) spec = resolve_deps(reqs, requirements)
else: else:
spec = combined_spec(args.reqs, args.requirements) spec = combined_spec(reqs, requirements)
if confirm( if confirm(
f"Write shim for {a.style(bin,'bold')} to {a.style(output,'green')}?", f"Write shim for {a.bold}{bin}{a.end} to {a.green}{output}{a.end}?",
yes=args.yes, yes=yes,
): ):
with output.open("w") as f: with output.open("w") as f:
f.write( f.write(t.shim(path, self.local_source, standalone, spec, bin))
t.shim(args.path, self.local_source, args.standalone, spec, bin)
)
make_executable(output) make_executable(output)
def run(self, args: Namespace) -> None: def run(
self,
reqs: List[str],
requirements: Path,
script: str,
keep: bool,
rest: List[str],
bin: str,
) -> None:
"""\ """\
run an app/script with an on-demand venv run an app/script with an on-demand venv
@ -1237,27 +1281,28 @@ class Viv:
viv r -s <remote python script> viv r -s <remote python script>
""" """
spec = combined_spec(args.reqs, args.requirements) spec = combined_spec(reqs, requirements)
if args.script: if script:
env = os.environ env = os.environ
name = args.script.split("/")[-1] name = script.split("/")[-1]
# TODO: reduce boilerplate and dry out # TODO: reduce boilerplate and dry out
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir: with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
tmppath = Path(tmpdir) tmppath = Path(tmpdir)
script = tmppath / name scriptpath = tmppath / name
if not self.local_source: if not self.local_source:
(tmppath / "viv.py").write_text( (tmppath / "viv.py").write_text(
# TODO: use latest tag once ready
fetch_script( fetch_script(
"https://raw.githubusercontent.com/daylinmorgan/viv/script-runner/src/viv/viv.py" "https://raw.githubusercontent.com/daylinmorgan/viv/script-runner/src/viv/viv.py"
) )
) )
script_text = fetch_script(args.script) script_text = fetch_script(script)
viv_used = uses_viv(script_text) viv_used = uses_viv(script_text)
script.write_text(script_text) scriptpath.write_text(script_text)
if not args.keep: if not keep:
env.update({"VIV_CACHE": tmpdir}) env.update({"VIV_CACHE": tmpdir})
os.environ["VIV_CACHE"] = tmpdir os.environ["VIV_CACHE"] = tmpdir
@ -1266,7 +1311,7 @@ class Viv:
sys.exit( sys.exit(
subprocess.run( subprocess.run(
[sys.executable, script, *args.rest], env=env [sys.executable, scriptpath, *rest], env=env
).returncode ).returncode
) )
else: else:
@ -1280,12 +1325,12 @@ class Viv:
sys.exit( sys.exit(
subprocess.run( subprocess.run(
[vivenv.path / "bin" / "python", script, *args.rest] [vivenv.path / "bin" / "python", script, *rest]
).returncode ).returncode
) )
else: else:
_, bin = self._pick_bin(args) _, bin = self._pick_bin(reqs, bin)
vivenv = ViVenv(spec) vivenv = ViVenv(spec)
# TODO: respect a VIV_RUN_MODE env variable as the same as keep i.e. # TODO: respect a VIV_RUN_MODE env variable as the same as keep i.e.
@ -1293,14 +1338,14 @@ class Viv:
# persist (use c.cache) # persist (use c.cache)
if not vivenv.loaded or Env().viv_force: if not vivenv.loaded or Env().viv_force:
if not args.keep: if not keep:
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir: with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
vivenv.path = Path(tmpdir) vivenv.path = Path(tmpdir)
vivenv.create() vivenv.create()
vivenv.install_pkgs() vivenv.install_pkgs()
sys.exit( sys.exit(
subprocess.run( subprocess.run(
[vivenv.path / "bin" / bin, *args.rest] [vivenv.path / "bin" / bin, *rest]
).returncode ).returncode
) )
else: else:
@ -1310,7 +1355,7 @@ class Viv:
vivenv.touch() vivenv.touch()
vivenv.meta.write() vivenv.meta.write()
sys.exit(subprocess.run([vivenv.path / "bin" / bin, *args.rest]).returncode) sys.exit(subprocess.run([vivenv.path / "bin" / bin, *rest]).returncode)
class Arg: class Arg:
@ -1350,17 +1395,22 @@ class Cli:
metavar="<path>", metavar="<path>",
), ),
], ],
("remove",): [Arg("vivenv", help="name/hash of vivenv", nargs="*")], ("remove",): [
Arg("vivenvs", help="name/hash of vivenv", nargs="*", metavar="vivenv")
],
("run",): [ ("run",): [
Arg("-s", "--script", help="remote script to run", metavar="<script>") Arg("-s", "--script", help="remote script to run", metavar="<script>")
], ],
("exe", "info"): [Arg("vivenv", help="name/hash of vivenv")], ("exe", "info"): [
Arg("vivenv_id", help="name/hash of vivenv", metavar="vivenv")
],
("list", "info"): [ ("list", "info"): [
Arg( Arg(
"--json", "--json",
help="name:metadata json for vivenvs ", help="name:metadata json for vivenvs ",
action="store_true", action="store_true",
default=False, default=False,
dest="use_json",
) )
], ],
("freeze", "shim"): [ ("freeze", "shim"): [
@ -1390,6 +1440,7 @@ class Cli:
"--requirements", "--requirements",
help="path/to/requirements.txt file", help="path/to/requirements.txt file",
metavar="<path>", metavar="<path>",
type=Path,
), ),
], ],
("run", "shim"): [ ("run", "shim"): [
@ -1467,7 +1518,7 @@ class Cli:
self.viv = viv self.viv = viv
self.parser = ArgumentParser(prog=viv.name, description=t.description) self.parser = ArgumentParser(prog=viv.name, description=t.description)
self._cmd_arg_group_map() self._cmd_arg_group_map()
self._make_parsers() self.parsers = self._make_parsers()
self._add_args() self._add_args()
def _cmd_arg_group_map(self) -> None: def _cmd_arg_group_map(self) -> None:
@ -1479,8 +1530,8 @@ class Cli:
for cmd in grp: for cmd in grp:
self.cmd_arg_group_map.setdefault(cmd, []).append(grp) self.cmd_arg_group_map.setdefault(cmd, []).append(grp)
def _make_parsers(self) -> None: def _make_parsers(self) -> Dict[Sequence[str] | str, ArgumentParser]:
self.parsers = {**{grp: ArgumentParser(add_help=False) for grp in self.args}} return {**{grp: ArgumentParser(add_help=False) for grp in self.args}}
def _add_args(self) -> None: def _add_args(self) -> None:
for grp, args in self.args.items(): for grp, args in self.args.items():
@ -1505,8 +1556,7 @@ class Cli:
if args.path and args.standalone: if args.path and args.standalone:
error("-p/--path and -s/--standalone are mutually exclusive", code=1) error("-p/--path and -s/--standalone are mutually exclusive", code=1)
if args.func.__name__ == "manage": if args.func.__name__ == "manage_install" and self.viv.local_source:
if args.subcmd == "install" and self.viv.local_source:
error(f"found existing viv installation at {self.viv.local_source}") error(f"found existing viv installation at {self.viv.local_source}")
echo( echo(
"use " "use "
@ -1515,7 +1565,8 @@ class Cli:
style="red", style="red",
) )
sys.exit(1) sys.exit(1)
if args.subcmd == "update":
if args.func.__name__ == "manage_update":
if not self.viv.local_source: if not self.viv.local_source:
error( error(
a.style("viv manage update", "bold") a.style("viv manage update", "bold")
@ -1584,7 +1635,8 @@ class Cli:
for k in self.cmd_arg_group_map[f"{cmd}|{subcmd}"] for k in self.cmd_arg_group_map[f"{cmd}|{subcmd}"]
], ],
**kwargs, **kwargs,
).set_defaults(func=getattr(self.viv, cmd), subcmd=subcmd) # ).set_defaults(func=getattr(self.viv, cmd), subcmd=subcmd)
).set_defaults(func=getattr(self.viv, f"{cmd}_{subcmd}"))
else: else:
self._get_subcmd_parser( self._get_subcmd_parser(
@ -1599,11 +1651,13 @@ class Cli:
args.rest = sys.argv[i + 1 :] args.rest = sys.argv[i + 1 :]
else: else:
args = self.parser.parse_args() args = self.parser.parse_args()
if args.func.__name__ in ("run", "exe"):
args.rest = [] args.rest = []
self._validate_args(args) self._validate_args(args)
args.func( func = args.__dict__.pop("func")
args, func(
**vars(args),
) )