mirror of
https://github.com/daylinmorgan/viv.git
synced 2024-12-23 02:50:44 -06:00
Compare commits
24 commits
3453004077
...
bfc2592925
Author | SHA1 | Date | |
---|---|---|---|
bfc2592925 | |||
8493916ff8 | |||
0d0c66d090 | |||
1e74ebf413 | |||
8fdb1817a7 | |||
ccf8e0ec61 | |||
992d039285 | |||
d367571957 | |||
cb27f9a8d4 | |||
9c2581fafd | |||
b9100ca2a1 | |||
e548487918 | |||
834cd449bd | |||
f646278f8b | |||
621f5a7aca | |||
03c5b06a8a | |||
28e57c99b4 | |||
1f11967f3f | |||
ff110f0829 | |||
9b5296036d | |||
54c9dbdf4b | |||
c797347aa6 | |||
10599dcd01 | |||
0d583584dc |
23 changed files with 512 additions and 94 deletions
|
@ -16,7 +16,14 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python
|
language_version: python
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
rev: 6.0.0
|
rev: 'v0.0.245'
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: ruff
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: set-version
|
||||||
|
language: script
|
||||||
|
name: sets __version__ in viv.py
|
||||||
|
entry: ./scripts/bump-dev.sh
|
||||||
|
files: viv.py$
|
||||||
|
|
10
Makefile
10
Makefile
|
@ -10,6 +10,7 @@ types: ## run mypy
|
||||||
bump-version: ## update version and tag commit
|
bump-version: ## update version and tag commit
|
||||||
@echo "bumping to version => $(VERSION)"
|
@echo "bumping to version => $(VERSION)"
|
||||||
@sed -i 's/__version__ = ".*"/__version__ = "$(VERSION)"/g' src/viv/viv.py
|
@sed -i 's/__version__ = ".*"/__version__ = "$(VERSION)"/g' src/viv/viv.py
|
||||||
|
@sed 's/--branch .* g/--branch $(VERSION) g/g' README.md
|
||||||
@git add src/viv/viv.py && git commit -m "chore: bump version"
|
@git add src/viv/viv.py && git commit -m "chore: bump version"
|
||||||
@git tag v$(VERSION)
|
@git tag v$(VERSION)
|
||||||
|
|
||||||
|
@ -22,11 +23,16 @@ install: ## symlink to $PREFIX
|
||||||
uninstall: ## delete $(PREFIX)/viv
|
uninstall: ## delete $(PREFIX)/viv
|
||||||
rm $(PREFIX)/viv
|
rm $(PREFIX)/viv
|
||||||
|
|
||||||
docs: docs/demo.gif docs/freeze.gif ## generate usage examples
|
TAPES = demo freeze list-info-remove
|
||||||
|
GIFS := $(foreach n, $(TAPES), docs/$(n).gif)
|
||||||
|
docs: $(GIFS) ## generate usage examples
|
||||||
|
|
||||||
docs/%.gif: docs/%.tape
|
docs/%.gif: docs/%.tape
|
||||||
viv rm $$(viv l -q)
|
viv rm $$(viv l -q)
|
||||||
vhs < $<
|
cd docs; vhs < $*.tape
|
||||||
|
|
||||||
|
clean: ## remove build artifacts
|
||||||
|
rm -rf {build,dist}
|
||||||
|
|
||||||
EXAMPLES = cli.py sys_path.py exe_specific.py frozen_import.py named_env.py scrape.py
|
EXAMPLES = cli.py sys_path.py exe_specific.py frozen_import.py named_env.py scrape.py
|
||||||
generate-example-vivens: ##
|
generate-example-vivens: ##
|
||||||
|
|
121
README.md
121
README.md
|
@ -11,14 +11,34 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
Start by cloning the repo and symlinking the script for access to the CLI.
|
Start by cloning the repo and symlinking the script for access to the CLI.
|
||||||
By default it will symlink `./src/viv/viv.py` to `~/bin/viv`.
|
By default it will symlink `./src/viv/viv.py` to `~/bin/viv`.
|
||||||
You can set `PREFIX` to symlink to a different location.
|
You can set `PREFIX` to symlink to a different location.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone git@github.com:daylinmorgan/viv.git ~/.viv
|
git clone --depth 1 --branch v22.12a3 git@github.com:daylinmorgan/viv.git ~/.viv
|
||||||
cd ~/.viv
|
cd ~/.viv
|
||||||
make install # or PREFIX=~/.local/bin make install
|
make install # or PREFIX=~/.local/bin make install
|
||||||
```
|
```
|
||||||
|
@ -29,20 +49,107 @@ Place this directory on the python path in your rc file.
|
||||||
export PYTHONPATH="$PYTHONPATH:$HOME/.viv/src"
|
export PYTHONPATH="$PYTHONPATH:$HOME/.viv/src"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then in any python script with external dependencies you can add this line.
|
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.
|
||||||
|
|
||||||
```python
|
### Pypi (Not Recommended)
|
||||||
__import__("viv").activate("click")
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
|
||||||
To remove all viv venvs:
|
In any python script with external dependencies you can add this line,
|
||||||
|
to automate `vivenv` creation and installation of dependencies.
|
||||||
|
|
||||||
|
```python
|
||||||
|
__import__("viv").use("click")
|
||||||
|
```
|
||||||
|
|
||||||
|
To remove all `vivenvs`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
viv remove $(viv list -q)
|
viv remove $(viv list -q)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# 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
|
||||||
|
no modification of your PYTHONPATH or import of additional modules (including downloading/installing `viv`).
|
||||||
|
|
||||||
|
It can be auto-generated with for example: `viv freeze <spec> --standalone`.
|
||||||
|
|
||||||
|
The only part necessary to modify if copied verbatim from below is the call to `_viv_use`.
|
||||||
|
|
||||||
|
output of `viv freeze rich --standalone`:
|
||||||
|
```python
|
||||||
|
# <<<<< auto-generated by daylinmorgan/viv (v.22.12a3)
|
||||||
|
# fmt: off
|
||||||
|
def _viv_use(*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
|
||||||
|
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
|
||||||
|
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True) # noqa
|
||||||
|
((sha256:=i("hashlib").sha256()).update((s(spec)+ # noqa
|
||||||
|
(((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode())) # 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa
|
||||||
|
_viv_use("markdown-it-py==2.2.0", "mdurl==0.1.2", "Pygments==2.14.0", "rich==13.3.2") # noqa
|
||||||
|
# fmt: on
|
||||||
|
# >>>>> code golfed with <3
|
||||||
|
```
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
||||||
- `pipx`
|
### [pip-run](https://github.com/jaraco/pip-run)
|
||||||
- `pip-run`
|
|
||||||
|
```sh
|
||||||
|
pip-run (10.0.5)
|
||||||
|
├── autocommand (2.2.2)
|
||||||
|
├── jaraco-context (4.3.0)
|
||||||
|
├── jaraco-functools (3.6.0)
|
||||||
|
│ └── more-itertools (9.1.0)
|
||||||
|
├── jaraco-text (3.11.1)
|
||||||
|
│ ├── autocommand (2.2.2)
|
||||||
|
│ ├── inflect (6.0.2)
|
||||||
|
│ │ └── pydantic>=1.9.1 (1.10.5)
|
||||||
|
│ │ └── typing-extensions>=4.2.0 (4.5.0)
|
||||||
|
│ ├── jaraco-context>=4.1 (4.3.0)
|
||||||
|
│ ├── jaraco-functools (3.6.0)
|
||||||
|
│ │ └── more-itertools (9.1.0)
|
||||||
|
│ └── more-itertools (9.1.0)
|
||||||
|
├── more-itertools>=8.3 (9.1.0)
|
||||||
|
├── packaging (23.0)
|
||||||
|
├── path>=15.1 (16.6.0)
|
||||||
|
├── pip>=19.3 (23.0.1)
|
||||||
|
└── platformdirs (3.1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### [pipx](https://github.com/pypa/pipx/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pipx (1.1.0)
|
||||||
|
├── argcomplete>=1.9.4 (2.1.1)
|
||||||
|
├── packaging>=20.0 (23.0)
|
||||||
|
└── userpath>=1.6.0 (1.8.0)
|
||||||
|
└── click (8.1.3)
|
||||||
|
```
|
||||||
|
|
11
docs/demo.py
Normal file
11
docs/demo.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
__import__("viv").use("pyfiglet==0.8.post1") # noqa
|
||||||
|
|
||||||
|
from pyfiglet import Figlet
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
f = Figlet(font="slant")
|
||||||
|
figtxt = f.renderText("viv").splitlines()
|
||||||
|
figtxt[-2] += " isn't venv!"
|
||||||
|
print("\n".join(figtxt))
|
|
@ -1,12 +1,12 @@
|
||||||
Set Height 750
|
Set Height 750
|
||||||
Set Width 1500
|
Set Width 1500
|
||||||
Set Theme "Catppuccin Mocha"
|
Set Theme "Catppuccin Mocha"
|
||||||
Output ./docs/demo.gif
|
Output ./demo.gif
|
||||||
|
|
||||||
Type "viv list"
|
Type "viv list"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
Type "python examples/cli.py --help"
|
Type "python ../examples/cli.py --help"
|
||||||
Enter
|
Enter
|
||||||
Sleep 10s
|
Sleep 10s
|
||||||
Type "viv list"
|
Type "viv list"
|
||||||
|
@ -15,9 +15,9 @@ Sleep 3s
|
||||||
Type "viv info 841"
|
Type "viv info 841"
|
||||||
Enter
|
Enter
|
||||||
Sleep 3s
|
Sleep 3s
|
||||||
Type "python examples/cli.py hello 'prospective viv user!'"
|
Type "python ../examples/cli.py hello 'prospective viv user!'"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
Type "python examples/cli.py goodbye 'prospective viv user!'"
|
Type "python ../examples/cli.py goodbye 'prospective viv user!'"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
Set Height 750
|
Set Height 750
|
||||||
Set Width 1500
|
Set Width 1500
|
||||||
Set Theme "Catppuccin Mocha"
|
Set Theme "Catppuccin Mocha"
|
||||||
Output ./docs/freeze.gif
|
Output ./freeze.gif
|
||||||
|
|
||||||
|
Type "viv freeze --help"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
Type "viv list"
|
Type "viv list"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
Type "viv freeze requests bs4"
|
Type "viv freeze requests bs4"
|
||||||
Enter
|
Enter
|
||||||
Sleep 5s
|
Sleep 7s
|
||||||
Type "viv list"
|
Type "viv list"
|
||||||
Enter
|
Enter
|
||||||
Sleep 3s
|
Sleep 3s
|
||||||
Type "viv freeze requests bs4 --keep --path relative"
|
Type "viv freeze requests bs4 --keep --path relative"
|
||||||
Enter
|
Enter
|
||||||
Sleep 5s
|
Sleep 7s
|
||||||
Type "viv list"
|
Type "viv list"
|
||||||
Enter
|
Enter
|
||||||
Sleep 5s
|
Sleep 5s
|
||||||
|
|
30
docs/list-info-remove.tape
Normal file
30
docs/list-info-remove.tape
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
Set Height 750
|
||||||
|
Set Width 1500
|
||||||
|
Set Theme "Catppuccin Mocha"
|
||||||
|
Output ./list-info-remove.gif
|
||||||
|
|
||||||
|
Type "cat demo.py"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type "python demo.py"
|
||||||
|
Enter
|
||||||
|
Sleep 3s
|
||||||
|
Type "viv list -h"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type "viv list -q"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "viv info -h"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type "viv info "
|
||||||
|
Sleep 1s
|
||||||
|
Type "f8"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "viv remove -h"
|
||||||
|
Enter
|
||||||
|
Type "viv rm f8"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
*NOTE*: these demo gif's are a work in progress. If you'd like to see them you can run `vhs` locally with `make docs`.
|
*NOTE*: these demo gif's are a work in progress. If you'd like to see them you can run `vhs` locally with `make docs`.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
# Demo
|
# Demo
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/demo.gif" alt="demo" width=600 >
|
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/demo.gif" alt="demo" width=600 >
|
||||||
|
@ -9,3 +10,9 @@
|
||||||
# Freeze
|
# Freeze
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/freeze.gif" alt="demo" width=600 >
|
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/freeze.gif" alt="demo" width=600 >
|
||||||
|
|
||||||
|
# List | Info | Remove
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/list-info-remove.gif" alt="demo" width=600 >
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@ It can be convenient to quickly generate a cli for a short script.
|
||||||
Or to add simple visualization of data using the wonderful rich library.
|
Or to add simple visualization of data using the wonderful rich library.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__import__("viv").activate("typer", "rich-click") # noqa
|
__import__("viv").use("typer", "rich-click") # noqa
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ it will generate a new vivenv.
|
||||||
It may be important to require a exe specificty if you are frequently running
|
It may be important to require a exe specificty if you are frequently running
|
||||||
different version of pythons and rely on c extension modules as in numpy.
|
different version of pythons and rely on c extension modules as in numpy.
|
||||||
"""
|
"""
|
||||||
__import__("viv").activate("numpy", "plotext", track_exe=True) # noqa
|
__import__("viv").use("numpy", "plotext", track_exe=True) # noqa
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import plotext as plt
|
import plotext as plt
|
||||||
|
|
|
@ -6,7 +6,7 @@ This import statement was generated using
|
||||||
Using viv freeze ensures future runs of this
|
Using viv freeze ensures future runs of this
|
||||||
script will use the same essential environment
|
script will use the same essential environment
|
||||||
"""
|
"""
|
||||||
__import__("viv").activate(
|
__import__("viv").use(
|
||||||
"numpy==1.24.0",
|
"numpy==1.24.0",
|
||||||
"pandas==1.5.2",
|
"pandas==1.5.2",
|
||||||
"python-dateutil==2.8.2",
|
"python-dateutil==2.8.2",
|
||||||
|
|
|
@ -7,7 +7,7 @@ Meaning that it will save it within the viv cache not using a hash.
|
||||||
*This environment could then be reused by specifying the name*
|
*This environment could then be reused by specifying the name*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__import__("viv").activate("rich", name="rich-env")
|
__import__("viv").use("rich", name="rich-env")
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
|
@ -6,7 +6,7 @@ modified from:
|
||||||
https://medium.com/analytics-vidhya/a-super-easy-python-script-for-web-scraping-that-anybody-can-use-d3bd6ab86c89
|
https://medium.com/analytics-vidhya/a-super-easy-python-script-for-web-scraping-that-anybody-can-use-d3bd6ab86c89
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__import__("viv").activate("requests", "bs4", "rich") # noqa
|
__import__("viv").use("requests", "bs4", "rich") # noqa
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
38
examples/standalone.py
Normal file
38
examples/standalone.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/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)
|
||||||
|
# fmt: off
|
||||||
|
def _viv_use(*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
|
||||||
|
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
|
||||||
|
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True) # noqa
|
||||||
|
((sha256:=i("hashlib").sha256()).update((s(spec)+ # noqa
|
||||||
|
(((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode())) # 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
sys.path = [p for p in (*sys.path,s(*(env/"lib").glob("py*/si*"))) if p!=i("site").USER_SITE] # noqa
|
||||||
|
_viv_use("pyfiglet==0.8.post1") # noqa
|
||||||
|
# fmt: on
|
||||||
|
# >>>>> code golfed with <3
|
||||||
|
|
||||||
|
from pyfiglet import Figlet
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
f = Figlet(font="slant")
|
||||||
|
figtxt = f.renderText("viv").splitlines()
|
||||||
|
figtxt[-2] += " isn't venv!"
|
||||||
|
print("\n".join(figtxt))
|
141
examples/stopwatch.py
Normal file
141
examples/stopwatch.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""A tui stopwatch built w/textual adapted from their tutorial:
|
||||||
|
https://github.com/Textualize/textual/tree/main/docs/examples/tutorial
|
||||||
|
"""
|
||||||
|
|
||||||
|
__import__("viv").use("textual")
|
||||||
|
|
||||||
|
from time import monotonic
|
||||||
|
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Container
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widgets import Button, Footer, Header, Static
|
||||||
|
|
||||||
|
|
||||||
|
class TimeDisplay(Static):
|
||||||
|
"""A widget to display elapsed time."""
|
||||||
|
|
||||||
|
start_time = reactive(monotonic)
|
||||||
|
time = reactive(0.0)
|
||||||
|
total = reactive(0.0)
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Event handler called when widget is added to the app."""
|
||||||
|
self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)
|
||||||
|
|
||||||
|
def update_time(self) -> None:
|
||||||
|
"""Method to update time to current."""
|
||||||
|
self.time = self.total + (monotonic() - self.start_time)
|
||||||
|
|
||||||
|
def watch_time(self, time: float) -> None:
|
||||||
|
"""Called when the time attribute changes."""
|
||||||
|
minutes, seconds = divmod(time, 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Method to start (or resume) time updating."""
|
||||||
|
self.start_time = monotonic()
|
||||||
|
self.update_timer.resume()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Method to stop the time display updating."""
|
||||||
|
self.update_timer.pause()
|
||||||
|
self.total += monotonic() - self.start_time
|
||||||
|
self.time = self.total
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Method to reset the time display to zero."""
|
||||||
|
self.total = 0
|
||||||
|
self.time = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Stopwatch(Static):
|
||||||
|
"""A stopwatch widget."""
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Event handler called when a button is pressed."""
|
||||||
|
button_id = event.button.id
|
||||||
|
time_display = self.query_one(TimeDisplay)
|
||||||
|
if button_id == "start":
|
||||||
|
time_display.start()
|
||||||
|
self.add_class("started")
|
||||||
|
elif button_id == "stop":
|
||||||
|
time_display.stop()
|
||||||
|
self.remove_class("started")
|
||||||
|
elif button_id == "reset":
|
||||||
|
time_display.reset()
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create child widgets of a stopwatch."""
|
||||||
|
yield Button("Start", id="start", variant="success")
|
||||||
|
yield Button("Stop", id="stop", variant="error")
|
||||||
|
yield Button("Reset", id="reset")
|
||||||
|
yield TimeDisplay()
|
||||||
|
|
||||||
|
|
||||||
|
class StopwatchApp(App):
|
||||||
|
"""A Textual app to manage stopwatches."""
|
||||||
|
|
||||||
|
# CSS_PATH = "stopwatch.css"
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
Stopwatch {
|
||||||
|
layout: horizontal;
|
||||||
|
background: $boost;
|
||||||
|
height: 5;
|
||||||
|
min-width: 50;
|
||||||
|
margin: 1;
|
||||||
|
padding: 1;
|
||||||
|
}
|
||||||
|
TimeDisplay {
|
||||||
|
content-align: center middle;
|
||||||
|
text-opacity: 60%;
|
||||||
|
height: 3;
|
||||||
|
}
|
||||||
|
Button { width: 16; }
|
||||||
|
#start { dock: left; }
|
||||||
|
#stop { dock: left; display: none; }
|
||||||
|
#reset { dock: right; }
|
||||||
|
.started {
|
||||||
|
text-style: bold;
|
||||||
|
background: $success;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
.started TimeDisplay { text-opacity: 100%; }
|
||||||
|
.started #start { display: none }
|
||||||
|
.started #stop { display: block }
|
||||||
|
.started #reset { visibility: hidden }"""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
("d", "toggle_dark", "Toggle dark mode"),
|
||||||
|
("a", "add_stopwatch", "Add"),
|
||||||
|
("r", "remove_stopwatch", "Remove"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Called to add widgets to the app."""
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
|
||||||
|
|
||||||
|
def action_add_stopwatch(self) -> None:
|
||||||
|
"""An action to add a timer."""
|
||||||
|
new_stopwatch = Stopwatch()
|
||||||
|
self.query_one("#timers").mount(new_stopwatch)
|
||||||
|
new_stopwatch.scroll_visible()
|
||||||
|
|
||||||
|
def action_remove_stopwatch(self) -> None:
|
||||||
|
"""Called to remove a timer."""
|
||||||
|
timers = self.query("Stopwatch")
|
||||||
|
if timers:
|
||||||
|
timers.last().remove()
|
||||||
|
|
||||||
|
def action_toggle_dark(self) -> None:
|
||||||
|
"""An action to toggle dark mode."""
|
||||||
|
self.dark = not self.dark
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = StopwatchApp()
|
||||||
|
app.run()
|
|
@ -7,7 +7,7 @@ Embed the viv.py on the sys.path at runtime rather than using PYTHONPATH
|
||||||
__import__("sys").path.append(
|
__import__("sys").path.append(
|
||||||
__import__("os").path.expanduser("~/.viv/src")
|
__import__("os").path.expanduser("~/.viv/src")
|
||||||
) # noqa # isort: off
|
) # noqa # isort: off
|
||||||
__import__("viv").activate("pyfiglet") # noqa # isort: off
|
__import__("viv").use("pyfiglet") # noqa # isort: off
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# ints are not allowed
|
# ints are not allowed
|
||||||
__import__("viv").activate(5)
|
__import__("viv").use(5)
|
||||||
|
|
|
@ -27,3 +27,11 @@ dev = [
|
||||||
"pre-commit>=3",
|
"pre-commit>=3",
|
||||||
"mypy>=0.991"
|
"mypy>=0.991"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
ignore = ["E402"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
warn_return_any = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
|
5
scripts/bump-dev.sh
Executable file
5
scripts/bump-dev.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
TAG=$(git describe --tags --always --dirty=-dev)
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/g" src/viv/viv.py
|
||||||
|
git add src/viv/viv.py
|
|
@ -1,8 +0,0 @@
|
||||||
[flake8]
|
|
||||||
max-line-length = 88
|
|
||||||
ignore = E402, W503
|
|
||||||
|
|
||||||
[mypy]
|
|
||||||
warn_return_any = True
|
|
||||||
check_untyped_defs = True
|
|
||||||
warn_unused_configs = True
|
|
|
@ -1 +1 @@
|
||||||
from .viv import activate # noqa
|
from .viv import use # noqa
|
||||||
|
|
171
src/viv/viv.py
171
src/viv/viv.py
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
viv -h
|
viv -h
|
||||||
OR
|
OR
|
||||||
__import__("viv").activate("requests", "bs4")
|
__import__("viv").use("requests", "bs4")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -30,7 +30,7 @@ from pathlib import Path
|
||||||
from textwrap import dedent, wrap
|
from textwrap import dedent, wrap
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
__version__ = "22.12a3"
|
__version__ = "22.12a3-35-g0d0c66d-dev"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -38,9 +38,7 @@ class Config:
|
||||||
"""viv config manager"""
|
"""viv config manager"""
|
||||||
|
|
||||||
venvcache: Path = (
|
venvcache: Path = (
|
||||||
Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".local" / "cache"))
|
Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) / "viv" / "venvs"
|
||||||
/ "viv"
|
|
||||||
/ "venvs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
@ -67,22 +65,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 +89,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 +134,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, "")
|
||||||
|
|
||||||
|
@ -196,7 +194,6 @@ class Ansi:
|
||||||
return sizes
|
return sizes
|
||||||
|
|
||||||
def _make_row(self, row) -> str:
|
def _make_row(self, row) -> str:
|
||||||
|
|
||||||
return f" {BOX['v']} " + f" {BOX['sep']} ".join(row) + f" {BOX['v']}"
|
return f" {BOX['v']} " + f" {BOX['sep']} ".join(row) + f" {BOX['v']}"
|
||||||
|
|
||||||
def _sanitize_row(self, sizes: List[int], row: Tuple[str]) -> Tuple[Tuple[str]]:
|
def _sanitize_row(self, sizes: List[int], row: Tuple[str]) -> Tuple[Tuple[str]]:
|
||||||
|
@ -249,9 +246,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()
|
||||||
|
@ -269,12 +266,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(
|
||||||
|
@ -334,14 +331,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:
|
||||||
|
@ -349,14 +347,14 @@ class ViVenv:
|
||||||
self,
|
self,
|
||||||
spec: List[str],
|
spec: List[str],
|
||||||
track_exe: bool = False,
|
track_exe: bool = False,
|
||||||
build_id: str | None = None,
|
id: str | None = None,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
path: Path | None = None,
|
path: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
self.exe = str(Path(sys.executable).resolve()) if track_exe else "N/A"
|
self.exe = str(Path(sys.executable).resolve()) if track_exe else "N/A"
|
||||||
self.build_id = build_id if build_id else get_hash(spec, track_exe)
|
self.id = id if id else get_hash(spec, track_exe)
|
||||||
self.name = name if name else self.build_id
|
self.name = name if name else self.id
|
||||||
self.path = path if path else c.venvcache / self.name
|
self.path = path if path else c.venvcache / self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -372,15 +370,12 @@ class ViVenv:
|
||||||
with (c.venvcache / name / "viv-info.json").open("r") as f:
|
with (c.venvcache / name / "viv-info.json").open("r") as f:
|
||||||
venvconfig = json.load(f)
|
venvconfig = json.load(f)
|
||||||
|
|
||||||
vivenv = cls(
|
vivenv = cls(name=name, spec=venvconfig["spec"], id=venvconfig["id"])
|
||||||
name=name, spec=venvconfig["spec"], build_id=venvconfig["build_id"]
|
|
||||||
)
|
|
||||||
vivenv.exe = venvconfig["exe"]
|
vivenv.exe = venvconfig["exe"]
|
||||||
|
|
||||||
return vivenv
|
return vivenv
|
||||||
|
|
||||||
def create(self) -> None:
|
def create(self) -> None:
|
||||||
|
|
||||||
echo(f"new unique vivenv -> {self.name}")
|
echo(f"new unique vivenv -> {self.name}")
|
||||||
with Spinner("creating vivenv"):
|
with Spinner("creating vivenv"):
|
||||||
builder = venv.EnvBuilder(with_pip=True, clear=True)
|
builder = venv.EnvBuilder(with_pip=True, clear=True)
|
||||||
|
@ -391,7 +386,6 @@ class ViVenv:
|
||||||
f.write("[global]\ndisable-pip-version-check = true")
|
f.write("[global]\ndisable-pip-version-check = true")
|
||||||
|
|
||||||
def install_pkgs(self):
|
def install_pkgs(self):
|
||||||
|
|
||||||
cmd: List[str | Path] = [
|
cmd: List[str | Path] = [
|
||||||
self.path / "bin" / "pip",
|
self.path / "bin" / "pip",
|
||||||
"install",
|
"install",
|
||||||
|
@ -406,12 +400,11 @@ class ViVenv:
|
||||||
)
|
)
|
||||||
|
|
||||||
def dump_info(self, write=False):
|
def dump_info(self, write=False):
|
||||||
|
|
||||||
# TODO: include associated files in 'info'
|
# TODO: include associated files in 'info'
|
||||||
# means it needs to be loaded first
|
# means it needs to be loaded first
|
||||||
info = {
|
info = {
|
||||||
"created": str(datetime.today()),
|
"created": str(datetime.today()),
|
||||||
"build_id": self.build_id,
|
"id": self.id,
|
||||||
"spec": self.spec,
|
"spec": self.spec,
|
||||||
"exe": self.exe,
|
"exe": self.exe,
|
||||||
}
|
}
|
||||||
|
@ -424,13 +417,13 @@ class ViVenv:
|
||||||
a.table((("key", "value"), *((k, v) for k, v in info.items())))
|
a.table((("key", "value"), *((k, v) for k, v in info.items())))
|
||||||
|
|
||||||
|
|
||||||
def activate(*packages: str, track_exe: bool = False, name: str = "") -> None:
|
def use(*packages: str, track_exe: bool = False, name: str = "") -> None:
|
||||||
"""create a vivenv and append to sys.path
|
"""create a vivenv and append to sys.path
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
packages: package specifications with optional version specifiers
|
packages: package specifications with optional version specifiers
|
||||||
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 build_id is used
|
name: use as vivenv name, if not provided id is used
|
||||||
"""
|
"""
|
||||||
validate_spec(packages)
|
validate_spec(packages)
|
||||||
vivenv = ViVenv(list(packages), track_exe=track_exe, name=name)
|
vivenv = ViVenv(list(packages), track_exe=track_exe, name=name)
|
||||||
|
@ -458,7 +451,6 @@ def validate_spec(spec):
|
||||||
|
|
||||||
|
|
||||||
def modify_sys_path(new_path: Path):
|
def modify_sys_path(new_path: Path):
|
||||||
|
|
||||||
# remove user-site
|
# remove user-site
|
||||||
for i, path in enumerate(sys.path):
|
for i, path in enumerate(sys.path):
|
||||||
if path == site.USER_SITE:
|
if path == site.USER_SITE:
|
||||||
|
@ -478,8 +470,48 @@ def get_venvs():
|
||||||
|
|
||||||
|
|
||||||
SYS_PATH_TEMPLATE = """__import__("sys").path.append("{path_to_viv}") # noqa"""
|
SYS_PATH_TEMPLATE = """__import__("sys").path.append("{path_to_viv}") # noqa"""
|
||||||
REL_SYS_PATH_TEMPLATE = """__import__("sys").path.append(__import__("os").path.expanduser("{path_to_viv}")) # noqa"""
|
REL_SYS_PATH_TEMPLATE = (
|
||||||
IMPORT_TEMPLATE = """__import__("viv").activate({spec}) # noqa"""
|
"""__import__("sys").path.append(__import__("os")"""
|
||||||
|
""".path.expanduser("{path_to_viv}")) # noqa"""
|
||||||
|
)
|
||||||
|
IMPORT_TEMPLATE = """__import__("viv").use({spec}) # noqa"""
|
||||||
|
|
||||||
|
STANDALONE_TEMPLATE = r"""
|
||||||
|
# <<<<< auto-generated by daylinmorgan/viv (v.22.12a3)
|
||||||
|
# fmt: off
|
||||||
|
{use}
|
||||||
|
# fmt: on
|
||||||
|
# >>>>> code golfed with <3
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
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
|
||||||
|
(cache:=(P(ge("XDG_CACHE_HOME",P.home()/".cache"))/"viv"/"venvs")).mkdir(parents=True,exist_ok=True)
|
||||||
|
((sha256:=i("hashlib").sha256()).update((s(spec)+
|
||||||
|
(((exe:=("N/A",s(P(i("sys").executable).resolve()))[e(track_exe)])))).encode()))
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
if (p:=i("subprocess").run([env/"bin"/"pip","install","--force-reinstall",*spec],text=True,
|
||||||
|
stdout=(-1,None)[v],stderr=(-2,None)[v])).returncode!=0:
|
||||||
|
if env.is_dir():i("shutil").rmtree(env)
|
||||||
|
ew(f"pip had non zero exit ({{p.returncode}})\n{{p.stdout}}\n");sys.exit(p.returncode)
|
||||||
|
with (env/"viv-info.json").open("w") as f:
|
||||||
|
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:
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def noqa(txt: str) -> str:
|
||||||
|
max_length = max(map(len, txt.splitlines()))
|
||||||
|
return "\n".join((f"{line:{max_length}} # noqa" for line in txt.splitlines()))
|
||||||
|
|
||||||
|
|
||||||
def spec_to_import(spec: List[str]) -> None:
|
def spec_to_import(spec: List[str]) -> None:
|
||||||
|
@ -508,7 +540,12 @@ def freeze_venv(spec: List[str], path: Path | None = None):
|
||||||
|
|
||||||
|
|
||||||
def generate_import(
|
def generate_import(
|
||||||
requirements: Path, reqs: List[str], vivenvs, include_path: bool, keep: bool
|
requirements: Path,
|
||||||
|
reqs: List[str],
|
||||||
|
vivenvs,
|
||||||
|
include_path: bool,
|
||||||
|
keep: bool,
|
||||||
|
standalone: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
# TODO: make compatible with Venv class for now just use the name /tmp/
|
# TODO: make compatible with Venv class for now just use the name /tmp/
|
||||||
reqs_from_file = []
|
reqs_from_file = []
|
||||||
|
@ -523,13 +560,13 @@ def generate_import(
|
||||||
echo("generating new vivenv")
|
echo("generating new vivenv")
|
||||||
vivenv, resolved_spec = freeze_venv(reqs + reqs_from_file)
|
vivenv, resolved_spec = freeze_venv(reqs + reqs_from_file)
|
||||||
|
|
||||||
# update build_id and move vivenv
|
# update id and move vivenv
|
||||||
vivenv.spec = resolved_spec.splitlines()
|
vivenv.spec = resolved_spec.splitlines()
|
||||||
vivenv.build_id = get_hash(resolved_spec.splitlines())
|
vivenv.id = get_hash(resolved_spec.splitlines())
|
||||||
echo(f"updated hash -> {vivenv.build_id}")
|
echo(f"updated hash -> {vivenv.id}")
|
||||||
|
|
||||||
if not (c.venvcache / vivenv.build_id).exists():
|
if not (c.venvcache / vivenv.id).exists():
|
||||||
vivenv.path = vivenv.path.rename(c.venvcache / vivenv.build_id)
|
vivenv.path = vivenv.path.rename(c.venvcache / vivenv.id)
|
||||||
vivenv.dump_info(write=True)
|
vivenv.dump_info(write=True)
|
||||||
else:
|
else:
|
||||||
echo("this vivenv already exists cleaning up temporary vivenv")
|
echo("this vivenv already exists cleaning up temporary vivenv")
|
||||||
|
@ -542,6 +579,21 @@ def generate_import(
|
||||||
)
|
)
|
||||||
|
|
||||||
echo("see below for import statements\n")
|
echo("see below for import statements\n")
|
||||||
|
|
||||||
|
if standalone:
|
||||||
|
sys.stdout.write(
|
||||||
|
STANDALONE_TEMPLATE.format(
|
||||||
|
version=__version__,
|
||||||
|
use=noqa(
|
||||||
|
STANDALONE_TEMPLATE_USE.format(
|
||||||
|
spec=", ".join(f'"{pkg}"' for pkg in resolved_spec.splitlines())
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if include_path == "absolute":
|
if include_path == "absolute":
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
SYS_PATH_TEMPLATE.format(
|
SYS_PATH_TEMPLATE.format(
|
||||||
|
@ -602,7 +654,6 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
||||||
return (", ").join(parts)
|
return (", ").join(parts)
|
||||||
|
|
||||||
def _format_usage(self, *args, **kwargs):
|
def _format_usage(self, *args, **kwargs):
|
||||||
|
|
||||||
formatted_usage = super()._format_usage(*args, **kwargs)
|
formatted_usage = super()._format_usage(*args, **kwargs)
|
||||||
# patch usage with color formatting
|
# patch usage with color formatting
|
||||||
formatted_usage = (
|
formatted_usage = (
|
||||||
|
@ -667,7 +718,6 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
||||||
|
|
||||||
def add_argument(self, action):
|
def add_argument(self, action):
|
||||||
if action.help is not SUPPRESS:
|
if action.help is not SUPPRESS:
|
||||||
|
|
||||||
# find all invocations
|
# find all invocations
|
||||||
get_invocation = self._format_action_invocation
|
get_invocation = self._format_action_invocation
|
||||||
invocations = [get_invocation(action)]
|
invocations = [get_invocation(action)]
|
||||||
|
@ -706,7 +756,7 @@ description = f"""
|
||||||
from command line:
|
from command line:
|
||||||
`{a.style("viv -h","bold")}`
|
`{a.style("viv -h","bold")}`
|
||||||
within python script:
|
within python script:
|
||||||
{a.style('__import__("viv").activate("typer", "rich-click")','bold')}
|
{a.style('__import__("viv").use("typer", "rich-click")','bold')}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -720,7 +770,7 @@ 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.build_id.startswith(name_id):
|
elif k.startswith(name_id) or v.id.startswith(name_id):
|
||||||
matches.append(v)
|
matches.append(v)
|
||||||
elif v.name.startswith(name_id):
|
elif v.name.startswith(name_id):
|
||||||
matches.append(v)
|
matches.append(v)
|
||||||
|
@ -755,11 +805,16 @@ class Viv:
|
||||||
"""create import statement from package spec"""
|
"""create import statement from package spec"""
|
||||||
|
|
||||||
if not args.reqs:
|
if not args.reqs:
|
||||||
print("must specify a requirement")
|
error("must specify a requirement")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
generate_import(
|
generate_import(
|
||||||
args.requirements, args.reqs, self.vivenvs, args.path, args.keep
|
args.requirements,
|
||||||
|
args.reqs,
|
||||||
|
self.vivenvs,
|
||||||
|
args.path,
|
||||||
|
args.keep,
|
||||||
|
args.standalone,
|
||||||
)
|
)
|
||||||
|
|
||||||
def list(self, args):
|
def list(self, args):
|
||||||
|
@ -791,7 +846,7 @@ class Viv:
|
||||||
|
|
||||||
pip_path, python_path = (vivenv.path / "bin" / cmd for cmd in ("pip", "python"))
|
pip_path, python_path = (vivenv.path / "bin" / cmd for cmd in ("pip", "python"))
|
||||||
# todo check for vivenv
|
# todo check for vivenv
|
||||||
print(f"executing command within {args.vivenv}")
|
echo(f"executing command within {args.vivenv}")
|
||||||
|
|
||||||
cmd = (
|
cmd = (
|
||||||
f"{pip_path} {' '.join(args.cmd)}"
|
f"{pip_path} {' '.join(args.cmd)}"
|
||||||
|
@ -912,6 +967,12 @@ class Viv:
|
||||||
help="preserve environment",
|
help="preserve environment",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
|
p_freeze.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--standalone",
|
||||||
|
help="generate standalone activation function",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
p_freeze.add_argument("reqs", help="requirements specifiers", nargs="*")
|
p_freeze.add_argument("reqs", help="requirements specifiers", nargs="*")
|
||||||
|
|
||||||
self._get_subcmd_parser(
|
self._get_subcmd_parser(
|
||||||
|
|
10
todo.md
10
todo.md
|
@ -1,7 +1,9 @@
|
||||||
# VIV Todo's
|
# VIV Todo's
|
||||||
|
|
||||||
- [ ] swap flake8 for ruff
|
- [x] swap flake8 for ruff
|
||||||
- [ ] add classifiers for pypi?
|
- [x] use stdout and stderr more effectively (or switch to logging?)
|
||||||
- [ ] use config file (probably ini or json / could also allow 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
|
- [ ] unit tests (v important)
|
||||||
|
|
||||||
|
- [ ] add more options to filter `viv list`
|
||||||
|
|
Loading…
Reference in a new issue