mirror of
https://github.com/daylinmorgan/viv.git
synced 2024-11-10 03:13:14 -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:
|
||||
- id: black
|
||||
language_version: python
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.245'
|
||||
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
|
||||
@echo "bumping to version => $(VERSION)"
|
||||
@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 tag v$(VERSION)
|
||||
|
||||
|
@ -22,11 +23,16 @@ install: ## symlink to $PREFIX
|
|||
uninstall: ## delete $(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
|
||||
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
|
||||
generate-example-vivens: ##
|
||||
|
|
121
README.md
121
README.md
|
@ -11,14 +11,34 @@
|
|||
</div>
|
||||
<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
|
||||
|
||||
### Manual (Recommended)
|
||||
|
||||
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`.
|
||||
You can set `PREFIX` to symlink to a different location.
|
||||
|
||||
```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
|
||||
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"
|
||||
```
|
||||
|
||||
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
|
||||
__import__("viv").activate("click")
|
||||
### Pypi (Not Recommended)
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
- `pipx`
|
||||
- `pip-run`
|
||||
### [pip-run](https://github.com/jaraco/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 Width 1500
|
||||
Set Theme "Catppuccin Mocha"
|
||||
Output ./docs/demo.gif
|
||||
Output ./demo.gif
|
||||
|
||||
Type "viv list"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type "python examples/cli.py --help"
|
||||
Type "python ../examples/cli.py --help"
|
||||
Enter
|
||||
Sleep 10s
|
||||
Type "viv list"
|
||||
|
@ -15,9 +15,9 @@ Sleep 3s
|
|||
Type "viv info 841"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Type "python examples/cli.py hello 'prospective viv user!'"
|
||||
Type "python ../examples/cli.py hello 'prospective viv user!'"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type "python examples/cli.py goodbye 'prospective viv user!'"
|
||||
Type "python ../examples/cli.py goodbye 'prospective viv user!'"
|
||||
Enter
|
||||
Sleep 2s
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
Set Height 750
|
||||
Set Width 1500
|
||||
Set Theme "Catppuccin Mocha"
|
||||
Output ./docs/freeze.gif
|
||||
Output ./freeze.gif
|
||||
|
||||
Type "viv freeze --help"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type "viv list"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type "viv freeze requests bs4"
|
||||
Enter
|
||||
Sleep 5s
|
||||
Sleep 7s
|
||||
Type "viv list"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Type "viv freeze requests bs4 --keep --path relative"
|
||||
Enter
|
||||
Sleep 5s
|
||||
Sleep 7s
|
||||
Type "viv list"
|
||||
Enter
|
||||
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`.
|
||||
|
||||
<div align="center">
|
||||
# Demo
|
||||
|
||||
<img src="https://raw.githubusercontent.com/daylinmorgan/viv/main/docs/demo.gif" alt="demo" width=600 >
|
||||
|
@ -9,3 +10,9 @@
|
|||
# Freeze
|
||||
|
||||
<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.
|
||||
"""
|
||||
|
||||
__import__("viv").activate("typer", "rich-click") # noqa
|
||||
__import__("viv").use("typer", "rich-click") # noqa
|
||||
|
||||
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
|
||||
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 plotext as plt
|
||||
|
|
|
@ -6,7 +6,7 @@ This import statement was generated using
|
|||
Using viv freeze ensures future runs of this
|
||||
script will use the same essential environment
|
||||
"""
|
||||
__import__("viv").activate(
|
||||
__import__("viv").use(
|
||||
"numpy==1.24.0",
|
||||
"pandas==1.5.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*
|
||||
"""
|
||||
|
||||
__import__("viv").activate("rich", name="rich-env")
|
||||
__import__("viv").use("rich", name="rich-env")
|
||||
|
||||
from rich.console import Console
|
||||
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
|
||||
"""
|
||||
|
||||
__import__("viv").activate("requests", "bs4", "rich") # noqa
|
||||
__import__("viv").use("requests", "bs4", "rich") # noqa
|
||||
|
||||
import requests
|
||||
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__("os").path.expanduser("~/.viv/src")
|
||||
) # noqa # isort: off
|
||||
__import__("viv").activate("pyfiglet") # noqa # isort: off
|
||||
__import__("viv").use("pyfiglet") # noqa # isort: off
|
||||
|
||||
import sys
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# ints are not allowed
|
||||
__import__("viv").activate(5)
|
||||
__import__("viv").use(5)
|
||||
|
|
|
@ -27,3 +27,11 @@ dev = [
|
|||
"pre-commit>=3",
|
||||
"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
|
||||
OR
|
||||
__import__("viv").activate("requests", "bs4")
|
||||
__import__("viv").use("requests", "bs4")
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
@ -30,7 +30,7 @@ from pathlib import Path
|
|||
from textwrap import dedent, wrap
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
__version__ = "22.12a3"
|
||||
__version__ = "22.12a3-35-g0d0c66d-dev"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -38,9 +38,7 @@ class Config:
|
|||
"""viv config manager"""
|
||||
|
||||
venvcache: Path = (
|
||||
Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".local" / "cache"))
|
||||
/ "viv"
|
||||
/ "venvs"
|
||||
Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) / "viv" / "venvs"
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
|
@ -67,22 +65,22 @@ class Spinner:
|
|||
def write_next(self):
|
||||
with self._screen_lock:
|
||||
if not self.spinner_visible:
|
||||
sys.stdout.write(next(self.spinner))
|
||||
sys.stderr.write(next(self.spinner))
|
||||
self.spinner_visible = True
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
def remove_spinner(self, cleanup=False):
|
||||
with self._screen_lock:
|
||||
if self.spinner_visible:
|
||||
sys.stdout.write("\b\b\b")
|
||||
sys.stderr.write("\b\b\b")
|
||||
# sys.stdout.write("\b")
|
||||
self.spinner_visible = False
|
||||
if cleanup:
|
||||
sys.stdout.write(" ") # overwrite spinner with blank
|
||||
sys.stderr.write(" ") # overwrite spinner with blank
|
||||
# sys.stdout.write("\r") # move to next line
|
||||
# move back then delete the line
|
||||
sys.stdout.write("\r\033[K")
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write("\r\033[K")
|
||||
sys.stderr.flush()
|
||||
|
||||
def spinner_task(self):
|
||||
while self.busy:
|
||||
|
@ -91,18 +89,18 @@ class Spinner:
|
|||
self.remove_spinner()
|
||||
|
||||
def __enter__(self):
|
||||
if sys.stdout.isatty():
|
||||
if sys.stderr.isatty():
|
||||
self._screen_lock = threading.Lock()
|
||||
self.busy = True
|
||||
self.thread = threading.Thread(target=self.spinner_task)
|
||||
self.thread.start()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_traceback): # noqa
|
||||
if sys.stdout.isatty():
|
||||
if sys.stderr.isatty():
|
||||
self.busy = False
|
||||
self.remove_spinner(cleanup=True)
|
||||
else:
|
||||
sys.stdout.write("\r")
|
||||
sys.stderr.write("\r")
|
||||
|
||||
|
||||
BOX: Dict[str, str] = {
|
||||
|
@ -136,7 +134,7 @@ class Ansi:
|
|||
metavar: str = "\033[33m" # normal yellow
|
||||
|
||||
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__:
|
||||
setattr(self, attr, "")
|
||||
|
||||
|
@ -196,7 +194,6 @@ class Ansi:
|
|||
return sizes
|
||||
|
||||
def _make_row(self, row) -> str:
|
||||
|
||||
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]]:
|
||||
|
@ -249,9 +246,9 @@ class Ansi:
|
|||
)
|
||||
)
|
||||
|
||||
sys.stdout.write(f" {BOX['tl']}{BOX['h']*(sum(sizes)+5)}{BOX['tr']}\n")
|
||||
sys.stdout.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['tl']}{BOX['h']*(sum(sizes)+5)}{BOX['tr']}\n")
|
||||
sys.stderr.write("\n".join(table_rows) + "\n")
|
||||
sys.stderr.write(f" {BOX['bl']}{BOX['h']*(sum(sizes)+5)}{BOX['br']}\n")
|
||||
|
||||
|
||||
a = Ansi()
|
||||
|
@ -269,12 +266,12 @@ def warn(msg):
|
|||
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 = f"{a.cyan}Viv{a.end}{a.__dict__[style]}::{a.end} {msg}"
|
||||
if newline:
|
||||
output += "\n"
|
||||
sys.stdout.write(output)
|
||||
fd.write(output)
|
||||
|
||||
|
||||
def run(
|
||||
|
@ -334,14 +331,15 @@ def get_hash(spec: Tuple[str, ...] | List[str], track_exe: bool = False) -> str:
|
|||
Returns:
|
||||
sha256 representation of dependencies for vivenv
|
||||
"""
|
||||
pkg_hash = hashlib.sha256()
|
||||
pkg_hash.update(str(spec).encode())
|
||||
|
||||
# generate unique venvs for unique python exe's
|
||||
if track_exe:
|
||||
pkg_hash.update(str(Path(sys.executable).resolve()).encode())
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(
|
||||
(
|
||||
str(spec) + (str(Path(sys.executable).resolve()) if track_exe else "N/A")
|
||||
).encode()
|
||||
)
|
||||
|
||||
return pkg_hash.hexdigest()
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
class ViVenv:
|
||||
|
@ -349,14 +347,14 @@ class ViVenv:
|
|||
self,
|
||||
spec: List[str],
|
||||
track_exe: bool = False,
|
||||
build_id: str | None = None,
|
||||
id: str | None = None,
|
||||
name: str = "",
|
||||
path: Path | None = None,
|
||||
) -> None:
|
||||
self.spec = spec
|
||||
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.name = name if name else self.build_id
|
||||
self.id = id if id else get_hash(spec, track_exe)
|
||||
self.name = name if name else self.id
|
||||
self.path = path if path else c.venvcache / self.name
|
||||
|
||||
@classmethod
|
||||
|
@ -372,15 +370,12 @@ class ViVenv:
|
|||
with (c.venvcache / name / "viv-info.json").open("r") as f:
|
||||
venvconfig = json.load(f)
|
||||
|
||||
vivenv = cls(
|
||||
name=name, spec=venvconfig["spec"], build_id=venvconfig["build_id"]
|
||||
)
|
||||
vivenv = cls(name=name, spec=venvconfig["spec"], id=venvconfig["id"])
|
||||
vivenv.exe = venvconfig["exe"]
|
||||
|
||||
return vivenv
|
||||
|
||||
def create(self) -> None:
|
||||
|
||||
echo(f"new unique vivenv -> {self.name}")
|
||||
with Spinner("creating vivenv"):
|
||||
builder = venv.EnvBuilder(with_pip=True, clear=True)
|
||||
|
@ -391,7 +386,6 @@ class ViVenv:
|
|||
f.write("[global]\ndisable-pip-version-check = true")
|
||||
|
||||
def install_pkgs(self):
|
||||
|
||||
cmd: List[str | Path] = [
|
||||
self.path / "bin" / "pip",
|
||||
"install",
|
||||
|
@ -406,12 +400,11 @@ class ViVenv:
|
|||
)
|
||||
|
||||
def dump_info(self, write=False):
|
||||
|
||||
# TODO: include associated files in 'info'
|
||||
# means it needs to be loaded first
|
||||
info = {
|
||||
"created": str(datetime.today()),
|
||||
"build_id": self.build_id,
|
||||
"id": self.id,
|
||||
"spec": self.spec,
|
||||
"exe": self.exe,
|
||||
}
|
||||
|
@ -424,13 +417,13 @@ class ViVenv:
|
|||
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
|
||||
|
||||
Args:
|
||||
packages: package specifications with optional version specifiers
|
||||
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)
|
||||
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):
|
||||
|
||||
# remove user-site
|
||||
for i, path in enumerate(sys.path):
|
||||
if path == site.USER_SITE:
|
||||
|
@ -478,8 +470,48 @@ def get_venvs():
|
|||
|
||||
|
||||
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"""
|
||||
IMPORT_TEMPLATE = """__import__("viv").activate({spec}) # noqa"""
|
||||
REL_SYS_PATH_TEMPLATE = (
|
||||
"""__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:
|
||||
|
@ -508,7 +540,12 @@ def freeze_venv(spec: List[str], path: Path | None = None):
|
|||
|
||||
|
||||
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:
|
||||
# TODO: make compatible with Venv class for now just use the name /tmp/
|
||||
reqs_from_file = []
|
||||
|
@ -523,13 +560,13 @@ def generate_import(
|
|||
echo("generating new vivenv")
|
||||
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.build_id = get_hash(resolved_spec.splitlines())
|
||||
echo(f"updated hash -> {vivenv.build_id}")
|
||||
vivenv.id = get_hash(resolved_spec.splitlines())
|
||||
echo(f"updated hash -> {vivenv.id}")
|
||||
|
||||
if not (c.venvcache / vivenv.build_id).exists():
|
||||
vivenv.path = vivenv.path.rename(c.venvcache / vivenv.build_id)
|
||||
if not (c.venvcache / vivenv.id).exists():
|
||||
vivenv.path = vivenv.path.rename(c.venvcache / vivenv.id)
|
||||
vivenv.dump_info(write=True)
|
||||
else:
|
||||
echo("this vivenv already exists cleaning up temporary vivenv")
|
||||
|
@ -542,6 +579,21 @@ def generate_import(
|
|||
)
|
||||
|
||||
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":
|
||||
sys.stdout.write(
|
||||
SYS_PATH_TEMPLATE.format(
|
||||
|
@ -602,7 +654,6 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
|||
return (", ").join(parts)
|
||||
|
||||
def _format_usage(self, *args, **kwargs):
|
||||
|
||||
formatted_usage = super()._format_usage(*args, **kwargs)
|
||||
# patch usage with color formatting
|
||||
formatted_usage = (
|
||||
|
@ -667,7 +718,6 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
|||
|
||||
def add_argument(self, action):
|
||||
if action.help is not SUPPRESS:
|
||||
|
||||
# find all invocations
|
||||
get_invocation = self._format_action_invocation
|
||||
invocations = [get_invocation(action)]
|
||||
|
@ -706,7 +756,7 @@ description = f"""
|
|||
from command line:
|
||||
`{a.style("viv -h","bold")}`
|
||||
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():
|
||||
if name_id == k or v.name == name_id:
|
||||
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)
|
||||
elif v.name.startswith(name_id):
|
||||
matches.append(v)
|
||||
|
@ -755,11 +805,16 @@ class Viv:
|
|||
"""create import statement from package spec"""
|
||||
|
||||
if not args.reqs:
|
||||
print("must specify a requirement")
|
||||
error("must specify a requirement")
|
||||
sys.exit(1)
|
||||
|
||||
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):
|
||||
|
@ -791,7 +846,7 @@ class Viv:
|
|||
|
||||
pip_path, python_path = (vivenv.path / "bin" / cmd for cmd in ("pip", "python"))
|
||||
# todo check for vivenv
|
||||
print(f"executing command within {args.vivenv}")
|
||||
echo(f"executing command within {args.vivenv}")
|
||||
|
||||
cmd = (
|
||||
f"{pip_path} {' '.join(args.cmd)}"
|
||||
|
@ -912,6 +967,12 @@ class Viv:
|
|||
help="preserve environment",
|
||||
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="*")
|
||||
|
||||
self._get_subcmd_parser(
|
||||
|
|
10
todo.md
10
todo.md
|
@ -1,7 +1,9 @@
|
|||
# VIV Todo's
|
||||
|
||||
- [ ] swap flake8 for ruff
|
||||
- [ ] add classifiers for pypi?
|
||||
- [ ] use config file (probably ini or json / could also allow toml for python>=3.11)
|
||||
- [x] swap flake8 for ruff
|
||||
- [x] use stdout and stderr more effectively (or switch to logging?)
|
||||
- [ ] use config file (probably ini or toml for python>=3.11)
|
||||
- [ ] 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