mirror of
https://github.com/daylinmorgan/viv.git
synced 2024-11-14 13:07:54 -06:00
Compare commits
6 commits
40aee738d1
...
fcb48daade
Author | SHA1 | Date | |
---|---|---|---|
fcb48daade | |||
fc30b5143b | |||
d0796ac350 | |||
7d98fd422d | |||
ae77c15827 | |||
df549555ea |
4 changed files with 194 additions and 222 deletions
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
|
@ -22,5 +22,6 @@ jobs:
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cname: viv.dayl.in
|
||||||
# publish_dir: ./site
|
# publish_dir: ./site
|
||||||
publish_dir: ./docs
|
publish_dir: ./docs
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
Try before you buy!
|
Try before you buy!
|
||||||
```sh
|
```sh
|
||||||
python3 <(curl -fsSL gh.dayl.in/viv/viv.py) run pycowsay -- "viv isn't venv\!"
|
python3 <(curl -fsSL viv.dayl.in/viv.py) run pycowsay -- "viv isn't venv\!"
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ For that reason any usage of the `CLI` can be accomplished using a remote copy a
|
||||||
Run the below command to install `viv`.
|
Run the below command to install `viv`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 <(curl -fsSL gh.dayl.in/viv/viv.py) manage install
|
python3 <(curl -fsSL viv.dayl.in/viv.py) manage install
|
||||||
```
|
```
|
||||||
|
|
||||||
To access `viv` from within scripts you should add it's location to your `PYTHONPATH`.
|
To access `viv` from within scripts you should add it's location to your `PYTHONPATH`.
|
||||||
|
@ -74,7 +74,7 @@ viv remove $(viv list -q)
|
||||||
To remove `viv` all together you can use the included `purge` command:
|
To remove `viv` all together you can use the included `purge` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 <(curl -fsSL gh.dayl.in/viv/viv.py) manage purge
|
python3 <(curl -fsSL viv.dayl.in/viv.py) manage purge
|
||||||
```
|
```
|
||||||
|
|
||||||
## Additional Features
|
## Additional Features
|
||||||
|
@ -87,7 +87,7 @@ After generating this a standalone `shim` you can freely use this script across
|
||||||
See [examples/black](https://github.com/daylinmorgan/viv/blob/dev/examples/black) for output of below command.
|
See [examples/black](https://github.com/daylinmorgan/viv/blob/dev/examples/black) for output of below command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 <(curl -fsSL gh.dayl.in/viv/viv.py) shim black -o ./black --standalone --freeze
|
python3 <(curl -fsSL viv.dayl.in/viv.py) shim black -o ./black --standalone --freeze
|
||||||
```
|
```
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
|
@ -2,4 +2,3 @@
|
||||||
TAG=$(git describe --tags --always --dirty=-dev --exclude 'latest')
|
TAG=$(git describe --tags --always --dirty=-dev --exclude 'latest')
|
||||||
VERSION="${TAG#v}"
|
VERSION="${TAG#v}"
|
||||||
sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/g" src/viv/viv.py
|
sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/g" src/viv/viv.py
|
||||||
git add src/viv/viv.py
|
|
||||||
|
|
406
src/viv/viv.py
406
src/viv/viv.py
|
@ -9,6 +9,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
@ -31,19 +32,17 @@ from argparse import (
|
||||||
_SubParsersAction,
|
_SubParsersAction,
|
||||||
)
|
)
|
||||||
from argparse import ArgumentParser as StdArgParser
|
from argparse import ArgumentParser as StdArgParser
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import zip_longest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from textwrap import dedent, fill, wrap
|
from textwrap import dedent, fill
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Dict,
|
Dict,
|
||||||
Generator,
|
|
||||||
List,
|
List,
|
||||||
NoReturn,
|
NoReturn,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
|
||||||
TextIO,
|
TextIO,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
|
@ -51,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.5a4-13-ga6bd81d-dev"
|
__version__ = "23.5a4-15-gdf54955-dev"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
@ -149,16 +148,6 @@ class Spinner:
|
||||||
class Ansi:
|
class Ansi:
|
||||||
"""control ouptut of ansi(VT100) control codes"""
|
"""control ouptut of ansi(VT100) control codes"""
|
||||||
|
|
||||||
BOX: Dict[str, str] = {
|
|
||||||
"v": "│",
|
|
||||||
"h": "─",
|
|
||||||
"tl": "╭",
|
|
||||||
"tr": "╮",
|
|
||||||
"bl": "╰",
|
|
||||||
"br": "╯",
|
|
||||||
"sep": "┆",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.bold: str = "\033[1m"
|
self.bold: str = "\033[1m"
|
||||||
self.dim: str = "\033[2m"
|
self.dim: str = "\033[2m"
|
||||||
|
@ -229,104 +218,9 @@ class Ansi:
|
||||||
new_output = [f"{self.red}->{self.end} {line}" for line in output.splitlines()]
|
new_output = [f"{self.red}->{self.end} {line}" for line in output.splitlines()]
|
||||||
sys.stdout.write("\n".join(new_output) + "\n")
|
sys.stdout.write("\n".join(new_output) + "\n")
|
||||||
|
|
||||||
def _get_column_sizes(
|
|
||||||
self, rows: Tuple[Tuple[str, Sequence[str]], ...]
|
|
||||||
) -> List[int]:
|
|
||||||
"""convert list of rows to list of columns sizes
|
|
||||||
|
|
||||||
First convert rows into list of columns,
|
|
||||||
then get max string length for each column.
|
|
||||||
"""
|
|
||||||
return list(max(map(len, lst)) for lst in map(list, zip(*rows))) # type: ignore
|
|
||||||
|
|
||||||
def _make_row(self, row: Generator[Any, None, None]) -> str:
|
|
||||||
return (
|
|
||||||
f" {self.BOX['v']} "
|
|
||||||
+ f" {self.BOX['sep']} ".join(row)
|
|
||||||
+ f" {self.BOX['v']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _sanitize_row(
|
|
||||||
self, sizes: List[int], row: Tuple[str, Sequence[str]]
|
|
||||||
) -> Tuple[Tuple[str, Sequence[str]], ...]:
|
|
||||||
if len(row[1]) > sizes[1]:
|
|
||||||
return tuple(
|
|
||||||
zip_longest(
|
|
||||||
(row[0],),
|
|
||||||
wrap(str(row[1]), break_on_hyphens=False, width=sizes[1]),
|
|
||||||
fillvalue="",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return (row,)
|
|
||||||
|
|
||||||
def viv_preamble(self, style: str = "magenta", sep: str = "::") -> str:
|
def viv_preamble(self, style: str = "magenta", sep: str = "::") -> str:
|
||||||
return f"{self.cyan}viv{self.end}{self.__dict__[style]}{sep}{self.end}"
|
return f"{self.cyan}viv{self.end}{self.__dict__[style]}{sep}{self.end}"
|
||||||
|
|
||||||
def table(
|
|
||||||
self, rows: Tuple[Tuple[str, Sequence[str]], ...], header_style: str = "cyan"
|
|
||||||
) -> None:
|
|
||||||
"""generate a table with outline and styled header assumes two columns
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rows: sequence of the rows, first item assumed to be header
|
|
||||||
header_style: color/style for header row
|
|
||||||
"""
|
|
||||||
|
|
||||||
sizes = self._get_column_sizes(rows)
|
|
||||||
|
|
||||||
col2_limit = shutil.get_terminal_size().columns - 20
|
|
||||||
if col2_limit < 20:
|
|
||||||
error("increase screen size to view table", code=1)
|
|
||||||
elif sizes[1] > col2_limit:
|
|
||||||
sizes[1] = col2_limit
|
|
||||||
|
|
||||||
header, rows = rows[0], rows[1:]
|
|
||||||
# this is maybe taking comprehensions too far....
|
|
||||||
table_rows = (
|
|
||||||
self._make_row(row)
|
|
||||||
for row in (
|
|
||||||
# header row
|
|
||||||
(
|
|
||||||
self.__dict__[header_style] + f"{cell:<{sizes[i]}}" + self.end
|
|
||||||
for i, cell in enumerate(header)
|
|
||||||
),
|
|
||||||
# rest of the rows
|
|
||||||
*(
|
|
||||||
(f"{cell:<{sizes[i]}}" for i, cell in enumerate(row))
|
|
||||||
for row in (
|
|
||||||
newrow
|
|
||||||
for row in rows
|
|
||||||
for newrow in self._sanitize_row(sizes, row)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
sys.stderr.write(
|
|
||||||
"".join(
|
|
||||||
(
|
|
||||||
" ",
|
|
||||||
self.BOX["tl"],
|
|
||||||
self.BOX["h"] * (sum(sizes) + 5),
|
|
||||||
self.BOX["tr"],
|
|
||||||
"\n",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sys.stderr.write("\n".join(table_rows) + "\n")
|
|
||||||
sys.stderr.write(
|
|
||||||
"".join(
|
|
||||||
(
|
|
||||||
" ",
|
|
||||||
self.BOX["bl"],
|
|
||||||
self.BOX["h"] * (sum(sizes) + 5),
|
|
||||||
self.BOX["br"],
|
|
||||||
"\n",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
a = Ansi()
|
a = Ansi()
|
||||||
|
|
||||||
|
@ -339,26 +233,45 @@ to create/activate a vivenv:
|
||||||
- from command line: `{a.style("viv -h","bold")}`
|
- from command line: `{a.style("viv -h","bold")}`
|
||||||
- within python script: {a.style('__import__("viv").use("typer", "rich-click")','bold')}
|
- within python script: {a.style('__import__("viv").use("typer", "rich-click")','bold')}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_standalone_func = r"""def _viv_use(*pkgs, track_exe=False, name=""):
|
_standalone_func = r"""def _viv_use(*pkgs, track_exe=False, name=""):
|
||||||
T,F,N=True,False,None;i,s,m,spec=__import__,str,map,[*pkgs]
|
import hashlib, json, os, site, shutil, sys, venv # noqa
|
||||||
e,w=lambda x: T if x else F,lambda p,t: p.write_text(t)
|
from pathlib import Path # noqa
|
||||||
if not {*m(type,pkgs)}=={s}: raise ValueError(f"spec: {pkgs} is invalid")
|
from datetime import datetime # noqa
|
||||||
ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write
|
from subprocess import run # noqa
|
||||||
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=T,exist_ok=T)
|
|
||||||
((sha256:=i("hashlib").sha256()).update((s(spec)+
|
if not {*map(type, pkgs)} == {str}:
|
||||||
(((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode()))
|
raise ValueError(f"spec: {pkgs} is invalid")
|
||||||
if {env:=cache/(((_id:=sha256.hexdigest()),name)[e(name)])}-{*cache.glob("*/")} or ge("VIV_FORCE"):
|
|
||||||
v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {env.name}\n")
|
meta = dict.fromkeys(("created", "accessed"), (t := str(datetime.today())))
|
||||||
i("venv").EnvBuilder(with_pip=T,clear=T).create(env)
|
runner = str(Path(__file__).absolute().resolve())
|
||||||
w(env/"pip.conf","[global]\ndisable-pip-version-check=true")
|
force, verbose, xdg = map(os.getenv, ("VIV_FORCE", "VIV_VERBOSE", "XDG_CACHE_HOME"))
|
||||||
if (rc:=(p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=T,
|
cache = (Path(xdg) if xdg else Path.home() / ".cache") / "viv" / "venvs"
|
||||||
stdout=(-1,N)[v],stderr=(-2,N)[v])).returncode)!=0:
|
cache.mkdir(parents=True, exist_ok=True)
|
||||||
if env.is_dir():i("shutil").rmtree(env)
|
exe = str(Path(sys.executable).resolve()) if track_exe else "N/A"
|
||||||
ew(f"pip had non zero exit ({rc})\n{p.stdout}\n");sys.exit(rc)
|
(sha256 := hashlib.sha256()).update((str(spec := [*pkgs]) + exe).encode())
|
||||||
w(env/"viv-info.json",i("json").dumps(
|
_id = sha256.hexdigest()
|
||||||
{"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe}))
|
if (env := cache / (name if name else _id)) not in cache.glob("*/") or force:
|
||||||
sys.path=[p for p in (*sys.path,s(*(env/"lib").glob("py*/si*")))if p!=i("site").USER_SITE]
|
sys.stderr.write(f"generating new vivenv -> {env.name}\n")
|
||||||
return env""" # noqa
|
venv.EnvBuilder(with_pip=True, clear=True).create(env)
|
||||||
|
(env / "pip.conf").write_text("[global]\ndisable-pip-version-check=true")
|
||||||
|
run_kw = dict(zip(("stdout", "stderr"), ((None,) * 2 if verbose else (-1, 2))))
|
||||||
|
p = run([env / "bin" / "pip", "install", "--force-reinstall", *spec], **run_kw)
|
||||||
|
if (rc := p.returncode) != 0:
|
||||||
|
if env.is_dir():
|
||||||
|
shutil.rmtree(env)
|
||||||
|
sys.stderr.write(f"pip had non zero exit ({rc})\n{p.stdout.decode()}\n")
|
||||||
|
sys.exit(rc)
|
||||||
|
meta.update(dict(id=_id, spec=spec, exe=exe, name=name, files=[runner]))
|
||||||
|
else:
|
||||||
|
meta = json.loads((env / "vivmeta.json").read_text())
|
||||||
|
meta.update(dict(accessed=t, files=sorted({*meta["files"],runner})))
|
||||||
|
|
||||||
|
(env / "vivmeta.json").write_text(json.dumps(meta))
|
||||||
|
sys.path = [p for p in sys.path if not p != site.USER_SITE]
|
||||||
|
site.addsitedir(str(*(env / "lib").glob("py*/si*")))
|
||||||
|
return env
|
||||||
|
"""
|
||||||
|
|
||||||
def noqa(self, txt: str) -> str:
|
def noqa(self, txt: str) -> str:
|
||||||
max_length = max(map(len, txt.splitlines()))
|
max_length = max(map(len, txt.splitlines()))
|
||||||
|
@ -372,16 +285,13 @@ to create/activate a vivenv:
|
||||||
return f"""__import__("viv").use({spec_str})"""
|
return f"""__import__("viv").use({spec_str})"""
|
||||||
|
|
||||||
def standalone(self, spec: List[str]) -> str:
|
def standalone(self, spec: List[str]) -> str:
|
||||||
func_use = self.noqa(
|
func_use = "\n".join(
|
||||||
"\n".join((self._standalone_func, self._use_str(spec, standalone=True)))
|
(self._standalone_func, self.noqa(self._use_str(spec, standalone=True)))
|
||||||
)
|
)
|
||||||
return f"""
|
return f"""
|
||||||
# see `python3 <(curl -fsSL gh.dayl.in/viv/viv.py) --help`
|
# see `python3 <(curl -fsSL viv.dayl.in/viv.py) --help`
|
||||||
# <<<<< auto-generated by viv (v{__version__})
|
# AUTOGENERATED by viv (v{__version__})
|
||||||
# fmt: off
|
|
||||||
{func_use}
|
{func_use}
|
||||||
# fmt: on
|
|
||||||
# >>>>> code golfed with <3
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _rel_import(self, local_source: Optional[Path]) -> str:
|
def _rel_import(self, local_source: Optional[Path]) -> str:
|
||||||
|
@ -426,9 +336,7 @@ to create/activate a vivenv:
|
||||||
bin: str,
|
bin: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
if standalone:
|
if standalone:
|
||||||
imports = "\n".join(
|
imports = self._standalone_func
|
||||||
("# fmt: off", self.noqa(self._standalone_func), "# fmt: on")
|
|
||||||
)
|
|
||||||
elif path == "abs":
|
elif path == "abs":
|
||||||
imports = self._absolute_import(local_source)
|
imports = self._absolute_import(local_source)
|
||||||
elif path == "rel":
|
elif path == "rel":
|
||||||
|
@ -724,37 +632,78 @@ def get_hash(spec: Tuple[str, ...] | List[str], track_exe: bool = False) -> str:
|
||||||
return sha256.hexdigest()
|
return sha256.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Meta:
|
||||||
|
name: str
|
||||||
|
id: str
|
||||||
|
spec: List[str]
|
||||||
|
files: List[str]
|
||||||
|
exe: str
|
||||||
|
created: str = ""
|
||||||
|
accessed: str = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, name: str) -> "Meta":
|
||||||
|
if not (c.venvcache / name / "vivmeta.json").exists():
|
||||||
|
warn(f"possibly corrupted vivenv: {name}")
|
||||||
|
# add empty values for corrupted vivenvs so it will still load
|
||||||
|
return cls(name=name, spec=[""], files=[""], exe="", id="")
|
||||||
|
else:
|
||||||
|
meta = json.loads((c.venvcache / name / "vivmeta.json").read_text())
|
||||||
|
|
||||||
|
return cls(**meta)
|
||||||
|
|
||||||
|
def write(self, p: Path | None = None) -> None:
|
||||||
|
if not p:
|
||||||
|
p = (c.venvcache) / self.name / "vivmeta.json"
|
||||||
|
|
||||||
|
p.write_text(json.dumps(self.__dict__))
|
||||||
|
|
||||||
|
def addfile(self, f: Path) -> None:
|
||||||
|
self.accessed = str(datetime.today())
|
||||||
|
self.files = sorted({*self.files, str(f.absolute().resolve())})
|
||||||
|
|
||||||
|
|
||||||
class ViVenv:
|
class ViVenv:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
spec: List[str],
|
spec: List[str] = [""],
|
||||||
track_exe: bool = False,
|
track_exe: bool = False,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
path: Path | None = None,
|
path: Path | None = None,
|
||||||
|
skip_load: bool = False,
|
||||||
|
metadata: Meta | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.spec = self._validate_spec(spec)
|
self.loaded = False
|
||||||
self.exe = str(Path(sys.executable).resolve()) if track_exe else "N/A"
|
spec = self._validate_spec(spec)
|
||||||
self.id = id if id else get_hash(spec, track_exe)
|
id = id if id else get_hash(spec, track_exe)
|
||||||
self.name = name if name else self.id
|
|
||||||
|
self.name = name if name else id
|
||||||
self.path = path if path else c.venvcache / self.name
|
self.path = path if path else c.venvcache / self.name
|
||||||
self.exists = self.name in [d.name for d in c.venvcache.iterdir()]
|
|
||||||
|
if not metadata:
|
||||||
|
if self.name in (d.name for d in c.venvcache.iterdir()):
|
||||||
|
self.loaded = True
|
||||||
|
self.meta = Meta.load(self.name)
|
||||||
|
else:
|
||||||
|
self.meta = Meta(
|
||||||
|
spec=spec,
|
||||||
|
name=self.name,
|
||||||
|
id=id,
|
||||||
|
files=[],
|
||||||
|
exe=str(Path(sys.executable).resolve()) if track_exe else "N/A",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.meta = metadata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, name: str) -> "ViVenv":
|
def load(cls, name: str) -> "ViVenv":
|
||||||
"""generate a vivenv from a viv-info.json file
|
"""generate a vivenv from a vivmeta.json
|
||||||
Args:
|
Args:
|
||||||
name: used as lookup in the vivenv cache
|
name: used as lookup in the vivenv cache
|
||||||
"""
|
"""
|
||||||
if not (c.venvcache / name / "viv-info.json").is_file():
|
vivenv = cls(name=name, metadata=Meta.load(name))
|
||||||
warn(f"possibly corrupted vivenv: {name}")
|
|
||||||
return cls(name=name, spec=[""])
|
|
||||||
else:
|
|
||||||
with (c.venvcache / name / "viv-info.json").open("r") as f:
|
|
||||||
venvconfig = json.load(f)
|
|
||||||
|
|
||||||
vivenv = cls(name=name, spec=venvconfig["spec"], id=venvconfig["id"])
|
|
||||||
vivenv.exe = venvconfig["exe"]
|
|
||||||
|
|
||||||
return vivenv
|
return vivenv
|
||||||
|
|
||||||
|
@ -781,12 +730,14 @@ class ViVenv:
|
||||||
with (self.path / "pip.conf").open("w") as f:
|
with (self.path / "pip.conf").open("w") as f:
|
||||||
f.write("[global]\ndisable-pip-version-check = true")
|
f.write("[global]\ndisable-pip-version-check = true")
|
||||||
|
|
||||||
|
self.meta.created = str(datetime.today())
|
||||||
|
|
||||||
def install_pkgs(self) -> None:
|
def install_pkgs(self) -> None:
|
||||||
cmd: List[str] = [
|
cmd: List[str] = [
|
||||||
str(self.path / "bin" / "pip"),
|
str(self.path / "bin" / "pip"),
|
||||||
"install",
|
"install",
|
||||||
"--force-reinstall",
|
"--force-reinstall",
|
||||||
] + self.spec
|
] + self.meta.spec
|
||||||
|
|
||||||
run(
|
run(
|
||||||
cmd,
|
cmd,
|
||||||
|
@ -795,24 +746,57 @@ class ViVenv:
|
||||||
verbose=bool(os.getenv("VIV_VERBOSE")),
|
verbose=bool(os.getenv("VIV_VERBOSE")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def dump_info(self, write: bool = False) -> None:
|
def show(self, verbose: bool = False) -> None:
|
||||||
# TODO: include associated files in 'info'
|
if not verbose:
|
||||||
# means it needs to be loaded first
|
_id = (
|
||||||
# or keep a seperate file hash in c.share?
|
self.meta.id[:8]
|
||||||
info = {
|
if self.meta.id == self.name
|
||||||
"created": str(datetime.today()),
|
else (self.name[:5] + "..." if len(self.name) > 8 else self.name)
|
||||||
"id": self.id,
|
)
|
||||||
"spec": self.spec,
|
|
||||||
"exe": self.exe,
|
|
||||||
}
|
|
||||||
|
|
||||||
# save metadata to json file
|
sys.stdout.write(
|
||||||
if write:
|
f"""{a.bold}{a.cyan}{_id}{a.end} """
|
||||||
with (self.path / "viv-info.json").open("w") as f:
|
f"""{a.style(", ".join(self.meta.spec),'dim')}\n"""
|
||||||
json.dump(info, f)
|
)
|
||||||
else:
|
else:
|
||||||
info["spec"] = ", ".join(self.spec)
|
self.tree()
|
||||||
a.table((("key", "value"), *((k, v) for k, v in info.items())))
|
|
||||||
|
def _tree_leaves(self, items: List[str], indent: str = "") -> str:
|
||||||
|
tree_chars = ["├"] * (len(items) - 1) + ["╰"]
|
||||||
|
return "\n".join(
|
||||||
|
(f"{indent}{a.yellow}{c}─{a.end} {i}" for c, i in zip(tree_chars, items))
|
||||||
|
)
|
||||||
|
|
||||||
|
def tree(self) -> None:
|
||||||
|
_id = self.meta.id if self.meta.id == self.name else self.name
|
||||||
|
# TODO: generarlize and loop this or make a template..
|
||||||
|
items = [
|
||||||
|
f"{a.magenta}{k}{a.end}: {v}"
|
||||||
|
for k, v in {
|
||||||
|
**{
|
||||||
|
"spec": ", ".join(self.meta.spec),
|
||||||
|
"created": self.meta.created,
|
||||||
|
"accessed": self.meta.accessed,
|
||||||
|
},
|
||||||
|
**({"exe": self.meta.exe} if self.meta.exe != "N/A" else {}),
|
||||||
|
**({"files": ""} if self.meta.files else {}),
|
||||||
|
}.items()
|
||||||
|
]
|
||||||
|
rows = [f"\n{a.bold}{a.cyan}{_id}{a.end}", self._tree_leaves(items)]
|
||||||
|
if self.meta.files:
|
||||||
|
rows += (self._tree_leaves(self.meta.files, indent=" "),)
|
||||||
|
|
||||||
|
sys.stdout.write("\n".join(rows) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def get_caller_path() -> Path:
|
||||||
|
"""get callers callers file path"""
|
||||||
|
# viv.py is fist in stack since function is used in `viv.use()`
|
||||||
|
frame_info = inspect.stack()[2]
|
||||||
|
filepath = frame_info.filename # in python 3.5+, you can use frame_info.filename
|
||||||
|
del frame_info # drop the reference to the stack frame to avoid reference cycles
|
||||||
|
|
||||||
|
return Path(filepath).absolute()
|
||||||
|
|
||||||
|
|
||||||
def use(*packages: str, track_exe: bool = False, name: str = "") -> Path:
|
def use(*packages: str, track_exe: bool = False, name: str = "") -> Path:
|
||||||
|
@ -823,26 +807,22 @@ def use(*packages: str, track_exe: bool = False, name: str = "") -> Path:
|
||||||
track_exe: if true make env python exe specific
|
track_exe: if true make env python exe specific
|
||||||
name: use as vivenv name, if not provided id is used
|
name: use as vivenv name, if not provided id is used
|
||||||
"""
|
"""
|
||||||
vivenv = ViVenv(list(packages), track_exe=track_exe, name=name)
|
|
||||||
|
|
||||||
if not vivenv.exists or os.getenv("VIV_FORCE"):
|
vivenv = ViVenv(list(packages), track_exe=track_exe, name=name)
|
||||||
|
if not vivenv.loaded or os.getenv("VIV_FORCE"):
|
||||||
vivenv.create()
|
vivenv.create()
|
||||||
vivenv.install_pkgs()
|
vivenv.install_pkgs()
|
||||||
vivenv.dump_info(write=True)
|
|
||||||
|
vivenv.meta.addfile(get_caller_path())
|
||||||
|
vivenv.meta.write()
|
||||||
|
|
||||||
modify_sys_path(vivenv.path)
|
modify_sys_path(vivenv.path)
|
||||||
return vivenv.path
|
return vivenv.path
|
||||||
|
|
||||||
|
|
||||||
def modify_sys_path(new_path: Path) -> None:
|
def modify_sys_path(new_path: Path) -> None:
|
||||||
# remove user-site
|
sys.path = [p for p in sys.path if p is not site.USER_SITE]
|
||||||
for i, path in enumerate(sys.path):
|
site.addsitedir(str(*(new_path / "lib").glob("python*/site-packages")))
|
||||||
if path == site.USER_SITE:
|
|
||||||
sys.path.pop(i)
|
|
||||||
|
|
||||||
sys.path.append(
|
|
||||||
str([p for p in (new_path / "lib").glob("python*/site-packages")][0])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_venvs() -> Dict[str, ViVenv]:
|
def get_venvs() -> Dict[str, ViVenv]:
|
||||||
|
@ -923,9 +903,7 @@ class Viv:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.vivenvs = get_venvs()
|
self.vivenvs = get_venvs()
|
||||||
self._get_sources()
|
self._get_sources()
|
||||||
self.name = (
|
self.name = "viv" if self.local else "python3 <(curl -fsSL viv.dayl.in/viv.py)"
|
||||||
"viv" if self.local else "python3 <(curl -fsSL gh.dayl.in/viv/viv.py)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_sources(self) -> None:
|
def _get_sources(self) -> None:
|
||||||
self.local_source: Optional[Path] = None
|
self.local_source: Optional[Path] = None
|
||||||
|
@ -954,7 +932,9 @@ class Viv:
|
||||||
for k, v in self.vivenvs.items():
|
for k, v in self.vivenvs.items():
|
||||||
if name_id == k or v.name == name_id:
|
if name_id == k or v.name == name_id:
|
||||||
matches.append(v)
|
matches.append(v)
|
||||||
elif k.startswith(name_id) or (v.id.startswith(name_id) and v.id == v.name):
|
elif k.startswith(name_id) or (
|
||||||
|
v.meta.id.startswith(name_id) and v.meta.id == v.name
|
||||||
|
):
|
||||||
matches.append(v)
|
matches.append(v)
|
||||||
elif v.name.startswith(name_id):
|
elif v.name.startswith(name_id):
|
||||||
matches.append(v)
|
matches.append(v)
|
||||||
|
@ -994,11 +974,10 @@ class Viv:
|
||||||
# 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)
|
||||||
|
|
||||||
if not vivenv.exists or os.getenv("VIV_FORCE"):
|
if not vivenv.loaded or os.getenv("VIV_FORCE"):
|
||||||
vivenv.create()
|
vivenv.create()
|
||||||
vivenv.install_pkgs()
|
vivenv.install_pkgs()
|
||||||
vivenv.dump_info(write=True)
|
vivenv.meta.write()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
echo("re-using existing vivenv")
|
echo("re-using existing vivenv")
|
||||||
|
|
||||||
|
@ -1021,19 +1000,8 @@ class Viv:
|
||||||
elif len(self.vivenvs) == 0:
|
elif len(self.vivenvs) == 0:
|
||||||
echo("no vivenvs setup")
|
echo("no vivenvs setup")
|
||||||
else:
|
else:
|
||||||
rows = (
|
for _, vivenv in self.vivenvs.items():
|
||||||
("vivenv", "spec"),
|
vivenv.show(args.verbose)
|
||||||
*(
|
|
||||||
(
|
|
||||||
f"{vivenv.name[:6]}..."
|
|
||||||
if len(vivenv.name) > 9
|
|
||||||
else vivenv.name,
|
|
||||||
", ".join(vivenv.spec),
|
|
||||||
)
|
|
||||||
for vivenv in self.vivenvs.values()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
a.table(rows)
|
|
||||||
|
|
||||||
def exe(self, args: Namespace) -> None:
|
def exe(self, args: Namespace) -> None:
|
||||||
"""run python/pip in existing vivenv"""
|
"""run python/pip in existing vivenv"""
|
||||||
|
@ -1056,14 +1024,12 @@ class Viv:
|
||||||
def info(self, args: Namespace) -> None:
|
def info(self, args: Namespace) -> None:
|
||||||
"""get metadata about a vivenv"""
|
"""get metadata about a vivenv"""
|
||||||
vivenv = self._match_vivenv(args.vivenv)
|
vivenv = self._match_vivenv(args.vivenv)
|
||||||
metadata_file = vivenv.path / "viv-info.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: {args.vivenv}", code=1)
|
||||||
|
|
||||||
echo(f"more info about {vivenv.name}:")
|
vivenv.show(verbose=True)
|
||||||
|
|
||||||
vivenv.dump_info()
|
|
||||||
|
|
||||||
def _install_local_src(self, sha256: str, src: Path, cli: Path) -> None:
|
def _install_local_src(self, sha256: str, src: Path, cli: Path) -> None:
|
||||||
echo("updating local source copy of viv")
|
echo("updating local source copy of viv")
|
||||||
|
@ -1071,12 +1037,11 @@ class Viv:
|
||||||
make_executable(src)
|
make_executable(src)
|
||||||
echo("symlinking cli")
|
echo("symlinking cli")
|
||||||
|
|
||||||
if cli.is_file() and confirm(
|
if cli.is_file():
|
||||||
f"Existing file at {a.style(str(cli),'bold')}, "
|
echo(f"Existing file at {a.style(str(cli),'bold')}")
|
||||||
"would you like to overwrite it?"
|
if confirm("Would you like to overwrite it?"):
|
||||||
):
|
cli.unlink()
|
||||||
cli.unlink()
|
cli.symlink_to(src)
|
||||||
cli.symlink_to(src)
|
|
||||||
else:
|
else:
|
||||||
cli.symlink_to(src)
|
cli.symlink_to(src)
|
||||||
|
|
||||||
|
@ -1172,7 +1137,7 @@ class Viv:
|
||||||
|
|
||||||
echo(
|
echo(
|
||||||
"to re-install use: "
|
"to re-install use: "
|
||||||
"`python3 <(curl -fsSL gh.dayl.in/viv/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, args: Namespace) -> Tuple[str, str]:
|
||||||
|
@ -1227,7 +1192,7 @@ class Viv:
|
||||||
# ephemeral (default), semi-ephemeral (persist inside /tmp), or
|
# ephemeral (default), semi-ephemeral (persist inside /tmp), or
|
||||||
# persist (use c.cache)
|
# persist (use c.cache)
|
||||||
|
|
||||||
if not vivenv.exists or os.getenv("VIV_FORCE"):
|
if not vivenv.loaded or os.getenv("VIV_FORCE"):
|
||||||
if not args.keep:
|
if not args.keep:
|
||||||
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
|
||||||
vivenv.path = Path(tmpdir)
|
vivenv.path = Path(tmpdir)
|
||||||
|
@ -1241,7 +1206,7 @@ class Viv:
|
||||||
else:
|
else:
|
||||||
vivenv.create()
|
vivenv.create()
|
||||||
vivenv.install_pkgs()
|
vivenv.install_pkgs()
|
||||||
vivenv.dump_info(write=True)
|
vivenv.meta.write()
|
||||||
|
|
||||||
sys.exit(subprocess.run([vivenv.path / "bin" / bin, *args.rest]).returncode)
|
sys.exit(subprocess.run([vivenv.path / "bin" / bin, *args.rest]).returncode)
|
||||||
|
|
||||||
|
@ -1265,7 +1230,7 @@ class Viv:
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def _validate_args(self, args):
|
def _validate_args(self, args: Namespace) -> None:
|
||||||
if args.func.__name__ in ("freeze", "shim", "run"):
|
if args.func.__name__ in ("freeze", "shim", "run"):
|
||||||
if not args.reqs:
|
if not args.reqs:
|
||||||
error("must specify a requirement", code=1)
|
error("must specify a requirement", code=1)
|
||||||
|
@ -1326,6 +1291,13 @@ class Viv:
|
||||||
p_vivenv_arg.add_argument("vivenv", help="name/hash of vivenv")
|
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(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
help="show full metadata for vivenvs",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
p_list.add_argument(
|
p_list.add_argument(
|
||||||
"-q",
|
"-q",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
|
|
Loading…
Reference in a new issue