Compare commits

...

5 commits

4 changed files with 109 additions and 71 deletions

View file

@ -13,6 +13,22 @@
See [usage](https://github.com/daylinmorgan/viv/blob/main/docs/usage.md) for more demo gifs. See [usage](https://github.com/daylinmorgan/viv/blob/main/docs/usage.md) for more demo gifs.
---
Python is a great choice to quickly prototype or accomplish small tasks in scripts.
However, leveraging it's vast ecosystem can be tedious for one-off or rarely used scripts.
This is were `viv` comes in handy.
`Viv` is a standalone dependency-free `venv` creator.
It is meant to be invoked in any script that has third-party dependencies,
prior to loading of any of the external modules.
These `venvs` can be identified by name or by their specification.
In any case they will be re-used across scripts (and generated on-demand, if needed).
**Importantly**, `viv` will remove your user site directory (`python -m 'import site;print(site.USER_SITE)'`),
to ensure the script isn't using anything outside the standard library and the `viv`-managed `venv`.
## Setup ## Setup
### Manual (Recommended) ### Manual (Recommended)
@ -33,57 +49,71 @@ Place this directory on the python path in your rc file.
export PYTHONPATH="$PYTHONPATH:$HOME/.viv/src" export PYTHONPATH="$PYTHONPATH:$HOME/.viv/src"
``` ```
Advanced users may recognize that principally,
the module just needs to be recognized at run time
and the single script at `./src/viv/viv.py` can be invoked directly for the CLI.
How you accomplish these options is ultimately up to you but the above instructions can get you started.
### Pypi (Not Recommended) ### Pypi (Not Recommended)
```sh ```sh
pip install viv pip install viv
``` ```
Why is this *not recommended*? Mainly, because `viv` is all about hacking your `sys.path`.
Placing it in it's own virtual environment or installing in a user site directory may complicate this endeavor.
## Usage ## Usage
Then in any python script with external dependencies you can add this line, In any python script with external dependencies you can add this line,
to automate env creation and installation of dependencies. to automate `vivenv` creation and installation of dependencies.
```python ```python
__import__("viv").activate("click") __import__("viv").activate("click")
``` ```
To remove all `vivenvs`: To remove all `vivenvs`:
```sh ```sh
viv remove $(viv list -q) viv remove $(viv list -q)
``` ```
## Standalone Function # Standalone Viv
*Requires* `python>=3.8`
Supposing you want to increase the portability of your script while still employing `viv`.
The below function can be freely pasted at the top of your scripts and requires The below function can be freely pasted at the top of your scripts and requires
no modification of your PYTHONPATH or import of additional modules. no modification of your PYTHONPATH or import of additional modules (including downloading/installing `viv`).
It can be auto-generated with for example: `viv freeze rich --standalone`. It can be auto-generated with for example: `viv freeze <spec> --standalone`.
The only part necessary to modify if copied verbatim from here is the call to `_viv_activate`. The only part necessary to modify if copied verbatim from below is the call to `_viv_activate`.
output of `viv freeze rich --standalone`:
```python ```python
# <<<<< auto-generated by daylinmorgan/viv (v.22.12a3) # <<<<< auto-generated by daylinmorgan/viv (v.22.12a3)
# fmt: off # fmt: off
def _viv_activate(*pkgs: str, track_exe: bool = False, name: str = "") -> None: # noqa def _viv_activate(*pkgs: str, track_exe: bool = False, name: str = "") -> None: # noqa
i,s,m,e,spec=__import__,str,map,lambda x: True if x else False, [*pkgs] # noqa i,s,m,e,spec=__import__,str,map,lambda x: True if x else False,[*pkgs] # noqa
if not {*m(type,pkgs)}=={s}: raise ValueError(f"spec: {pkgs} is invalid") # noqa if not {*m(type,pkgs)}=={s}: raise ValueError(f"spec: {pkgs} is invalid") # noqa
ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write # noqa ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write # noqa
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True, exist_ok=True) # noqa (cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True) # noqa
((hash:=i("hashlib").sha256()).update((s(spec)+ # noqa ((sha256:=i("hashlib").sha256()).update((s(spec)+ # noqa
(((exe:=s(P(i("sys").executable).resolve()) if track_exe else "N/A")))).encode())) # noqa (((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode())) # noqa
if (env:=cache/(name if name else (_id:=hash.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"): # noqa if (env:=cache/(name if name else (_id:=sha256.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"): # noqa
v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {env.name}\n") # noqa v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {env.name}\n") # noqa
i("venv").EnvBuilder(with_pip=True,clear=True).create(env) # noqa i("venv").EnvBuilder(with_pip=True,clear=True).create(env) # noqa
with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true") # noqa with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true") # noqa
if (p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=True, # noqa if (p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=True, # noqa
stdout=(-1,None)[v],stderr=(-2,None)[v])).returncode!=0: # noqa stdout=(-1,None)[v],stderr=(-2,None)[v])).returncode!=0: # noqa
if env.is_dir():i("shutil").rmtree(env) # noqa if env.is_dir():i("shutil").rmtree(env) # noqa
ew(f"pip had non zero exit ({p.returncode})\n{p.stdout}\n");sys.exit(p.returncode) # noqa ew(f"pip had non zero exit ({p.returncode})\n{p.stdout}\n");sys.exit(p.returncode) # noqa
with (env/"viv-info.json").open("w") as f: # noqa with (env/"viv-info.json").open("w") as f: # noqa
i("json").dump({"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe},f) # noqa i("json").dump({"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe},f) # noqa
sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa
_viv_activate("markdown-it-py==2.2.0", "mdurl==0.1.2", "Pygments==2.14.0", "rich==13.3.2") # noqa _viv_activate("markdown-it-py==2.2.0", "mdurl==0.1.2", "Pygments==2.14.0", "rich==13.3.2") # noqa
# fmt: on # fmt: on
# >>>>> code golfed with <3 # >>>>> code golfed with <3
``` ```

View file

@ -1,26 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Example using the output of `viv freeze pyfiglet --standalone`.
With this function it's not necessary for
`viv` to exist anywhere on the system.
"""
# <<<<< auto-generated by daylinmorgan/viv (v.22.12a3) # <<<<< auto-generated by daylinmorgan/viv (v.22.12a3)
# fmt: off # fmt: off
def _viv_activate(*pkgs: str, track_exe: bool = False, name: str = "") -> None: # noqa def _viv_activate(*pkgs: str, track_exe: bool = False, name: str = "") -> None: # noqa
i,s,m,e,spec=__import__,str,map,lambda x: True if x else False, [*pkgs] # noqa i,s,m,e,spec=__import__,str,map,lambda x: True if x else False,[*pkgs] # noqa
if not {*m(type,pkgs)}=={s}: raise ValueError(f"spec: {pkgs} is invalid") # noqa if not {*m(type,pkgs)}=={s}: raise ValueError(f"spec: {pkgs} is invalid") # noqa
ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write # noqa ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write # noqa
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True, exist_ok=True) # noqa (cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True) # noqa
((hash:=i("hashlib").sha256()).update((s(spec)+ # noqa ((sha256:=i("hashlib").sha256()).update((s(spec)+ # noqa
(((exe:=s(P(i("sys").executable).resolve()) if track_exe else "N/A")))).encode())) # noqa (((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode())) # noqa
if (env:=cache/(name if name else (_id:=hash.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"): # noqa if (env:=cache/(name if name else (_id:=sha256.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"): # noqa
v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {env.name}\n") # noqa v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {env.name}\n") # noqa
i("venv").EnvBuilder(with_pip=True,clear=True).create(env) # noqa i("venv").EnvBuilder(with_pip=True,clear=True).create(env) # noqa
with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true") # noqa with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true") # noqa
if (p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=True, # noqa if (p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=True, # noqa
stdout=(-1,None)[v],stderr=(-2,None)[v])).returncode!=0: # noqa stdout=(-1,None)[v],stderr=(-2,None)[v])).returncode!=0: # noqa
if env.is_dir():i("shutil").rmtree(env) # noqa if env.is_dir():i("shutil").rmtree(env) # noqa
ew(f"pip had non zero exit ({p.returncode})\n{p.stdout}\n");sys.exit(p.returncode) # noqa ew(f"pip had non zero exit ({p.returncode})\n{p.stdout}\n");sys.exit(p.returncode) # noqa
with (env/"viv-info.json").open("w") as f: # noqa with (env/"viv-info.json").open("w") as f: # noqa
i("json").dump({"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe},f) # noqa i("json").dump({"created":s(i("datetime").datetime.today()),"id":_id,"spec":spec,"exe":exe},f) # noqa
sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa
_viv_activate("pyfiglet==0.8.post1") # noqa _viv_activate("pyfiglet==0.8.post1") # noqa
# fmt: on # fmt: on
# >>>>> code golfed with <3 # >>>>> code golfed with <3

View file

@ -67,22 +67,22 @@ class Spinner:
def write_next(self): def write_next(self):
with self._screen_lock: with self._screen_lock:
if not self.spinner_visible: if not self.spinner_visible:
sys.stdout.write(next(self.spinner)) sys.stderr.write(next(self.spinner))
self.spinner_visible = True self.spinner_visible = True
sys.stdout.flush() sys.stderr.flush()
def remove_spinner(self, cleanup=False): def remove_spinner(self, cleanup=False):
with self._screen_lock: with self._screen_lock:
if self.spinner_visible: if self.spinner_visible:
sys.stdout.write("\b\b\b") sys.stderr.write("\b\b\b")
# sys.stdout.write("\b") # sys.stdout.write("\b")
self.spinner_visible = False self.spinner_visible = False
if cleanup: if cleanup:
sys.stdout.write(" ") # overwrite spinner with blank sys.stderr.write(" ") # overwrite spinner with blank
# sys.stdout.write("\r") # move to next line # sys.stdout.write("\r") # move to next line
# move back then delete the line # move back then delete the line
sys.stdout.write("\r\033[K") sys.stderr.write("\r\033[K")
sys.stdout.flush() sys.stderr.flush()
def spinner_task(self): def spinner_task(self):
while self.busy: while self.busy:
@ -91,18 +91,18 @@ class Spinner:
self.remove_spinner() self.remove_spinner()
def __enter__(self): def __enter__(self):
if sys.stdout.isatty(): if sys.stderr.isatty():
self._screen_lock = threading.Lock() self._screen_lock = threading.Lock()
self.busy = True self.busy = True
self.thread = threading.Thread(target=self.spinner_task) self.thread = threading.Thread(target=self.spinner_task)
self.thread.start() self.thread.start()
def __exit__(self, exc_type, exc_val, exc_traceback): # noqa def __exit__(self, exc_type, exc_val, exc_traceback): # noqa
if sys.stdout.isatty(): if sys.stderr.isatty():
self.busy = False self.busy = False
self.remove_spinner(cleanup=True) self.remove_spinner(cleanup=True)
else: else:
sys.stdout.write("\r") sys.stderr.write("\r")
BOX: Dict[str, str] = { BOX: Dict[str, str] = {
@ -136,7 +136,7 @@ class Ansi:
metavar: str = "\033[33m" # normal yellow metavar: str = "\033[33m" # normal yellow
def __post_init__(self): def __post_init__(self):
if os.getenv("NO_COLOR") or not sys.stdout.isatty(): if os.getenv("NO_COLOR") or not sys.stderr.isatty():
for attr in self.__dict__: for attr in self.__dict__:
setattr(self, attr, "") setattr(self, attr, "")
@ -248,9 +248,9 @@ class Ansi:
) )
) )
sys.stdout.write(f" {BOX['tl']}{BOX['h']*(sum(sizes)+5)}{BOX['tr']}\n") sys.stderr.write(f" {BOX['tl']}{BOX['h']*(sum(sizes)+5)}{BOX['tr']}\n")
sys.stdout.write("\n".join(table_rows) + "\n") sys.stderr.write("\n".join(table_rows) + "\n")
sys.stdout.write(f" {BOX['bl']}{BOX['h']*(sum(sizes)+5)}{BOX['br']}\n") sys.stderr.write(f" {BOX['bl']}{BOX['h']*(sum(sizes)+5)}{BOX['br']}\n")
a = Ansi() a = Ansi()
@ -268,12 +268,12 @@ def warn(msg):
echo(f"{a.yellow}warn:{a.end} {msg}", style="yellow") echo(f"{a.yellow}warn:{a.end} {msg}", style="yellow")
def echo(msg: str, style="magenta", newline=True) -> None: def echo(msg: str, style="magenta", newline=True, fd=sys.stderr) -> None:
"""output general message to stdout""" """output general message to stdout"""
output = f"{a.cyan}Viv{a.end}{a.__dict__[style]}::{a.end} {msg}" output = f"{a.cyan}Viv{a.end}{a.__dict__[style]}::{a.end} {msg}"
if newline: if newline:
output += "\n" output += "\n"
sys.stdout.write(output) fd.write(output)
def run( def run(
@ -333,14 +333,15 @@ def get_hash(spec: Tuple[str, ...] | List[str], track_exe: bool = False) -> str:
Returns: Returns:
sha256 representation of dependencies for vivenv sha256 representation of dependencies for vivenv
""" """
pkg_hash = hashlib.sha256()
pkg_hash.update(str(spec).encode())
# generate unique venvs for unique python exe's sha256 = hashlib.sha256()
if track_exe: sha256.update(
pkg_hash.update(str(Path(sys.executable).resolve()).encode()) (
str(spec) + (str(Path(sys.executable).resolve()) if track_exe else "N/A")
).encode()
)
return pkg_hash.hexdigest() return sha256.hexdigest()
class ViVenv: class ViVenv:
@ -487,13 +488,13 @@ STANDALONE_TEMPLATE = r"""
STANDALONE_TEMPLATE_ACTIVATE = r""" STANDALONE_TEMPLATE_ACTIVATE = r"""
def _viv_activate(*pkgs: str, track_exe: bool = False, name: str = "") -> None: def _viv_activate(*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] 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") 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 ge,sys,P,ew=i("os").getenv,i("sys"),i("pathlib").Path,i("sys").stderr.write
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True, exist_ok=True) (cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True)
((hash:=i("hashlib").sha256()).update((s(spec)+ ((sha256:=i("hashlib").sha256()).update((s(spec)+
(((exe:=s(P(i("sys").executable).resolve()) if track_exe else "N/A")))).encode())) (((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode()))
if (env:=cache/(name if name else (_id:=hash.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"): if (env:=cache/(name if name else (_id:=sha256.hexdigest()))) not in cache.glob("*/") or ge("VIV_FORCE"):
v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {{env.name}}\n") v=e(ge("VIV_VERBOSE"));ew(f"generating new vivenv -> {{env.name}}\n")
i("venv").EnvBuilder(with_pip=True,clear=True).create(env) i("venv").EnvBuilder(with_pip=True,clear=True).create(env)
with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true") with (env/"pip.conf").open("w") as f:f.write("[global]\ndisable-pip-version-check=true")

View file

@ -1,7 +1,9 @@
# VIV Todo's # VIV Todo's
- [x] swap flake8 for ruff - [x] swap flake8 for ruff
- [ ] use stdout and stderr more effectively (or switch to logging?) - [x] use stdout and stderr more effectively (or switch to logging?)
- [ ] use config file (probably ini or toml for python>=3.11) - [ ] use config file (probably ini or toml for python>=3.11)
- [ ] enable a garbage collection based on time or file existence (configurable) - [ ] enable a garbage collection based on time or file existence (configurable)
- [ ] unit tests (v important) - [ ] unit tests (v important)
- [ ] add more options to filter `viv list`