Compare commits

...

5 Commits

11 changed files with 431 additions and 203 deletions

View File

@ -29,7 +29,7 @@
Try before you buy!
```sh
python3 <(curl -fsSL viv.dayl.in/viv.py) run pycowsay -- "viv isn't venv\!"
python3 <(curl -fsSL viv.dayl.in/viv.py) run frogmouth -- gh daylinmorgan/viv
```
---

View File

@ -3,6 +3,7 @@ copyright = "2023, Daylin Morgan"
author = "Daylin Morgan"
extensions = ["myst_parser", "sphinx_copybutton"]
myst_enable_extensions = ["colon_fence", "deflist"]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@ -13,4 +14,10 @@ html_theme = "shibuya"
# html_static_path = ["_static"]
html_logo = "../assets/logo.svg"
html_theme_options = {"github_url": "https://github.com/daylinmorgan/viv"}
html_theme_options = {
"github_url": "https://github.com/daylinmorgan/viv",
"nav_links": [
{"title": "Documentation", "url": "installation"},
{"title": "CLI Reference", "url": "cli"},
],
}

45
docs/configuration.md Normal file
View File

@ -0,0 +1,45 @@
# Configuration
## Environment Variables
`VIV_RUN_MODE`
: **ephemeral** (default):
: `viv run` will generate a temporary directory that is removed following execution
: **semi-ephemeral**
: `viv run` will set the `VIV_CACHE` the directory to `$TEMPDIR/viv-ephemeral-cache-$USER`
: persist
: `viv run` will always use the standard `VIV_CACHE` which maximizes reusable vivenvs
`VIV_CACHE`
: Path to use for vivenv cache by default `$XDG_CACHE_HOME/viv` or `$HOME/.cache/viv`
`VIV_LOG_PATH`
: Path to use log file by default `$XDG_DATA_HOME/viv/viv.log` or `$HOME/.local/share/viv/viv.log`
`VIV_BIN_DIR`
: Path to use for shims by default `$HOME/.local/bin`
`VIV_NO_SETUPTOOLS`
: Don't add setuptools to generated vivenvs.
: Many legacy packages expect setuptools to be available
and don't appropriate declare it as dependency.
To minimize frustration `setuptools` is added to every dependency
list.
`VIV_FORCE`
: Remove existence check and recreate vivenv
`VIV_SPEC`
: Space separated list of dependencies in addition to those in script
`VIV_VERBOSE`
: Show `pip` output in real time
`VIV_DEBUG`
: Set log level to `DEBUG`
`FORCE_COLOR`
: Force output to use ANSI escape codes
`NO_COLOR`
: Remove all ANSI escape codes from output

View File

@ -1,31 +1,41 @@
---
hide-toc: true
layout: landing
cover: http://viv.dayl.in/_static/logo.svg
desciption: Viv isn't venv
---
# viv isn't venv
Try before you buy!
```sh
python3 <(curl -fsSL viv.dayl.in/viv.py) run pycowsay -- "viv isn't venv\!"
python3 <(curl -fsSL viv.dayl.in/viv.py) run frogmouth -- gh daylinmorgan/viv
```
---
`Viv` is a standalone dependency-free `venv` creator [^1].
:::{container} buttons
[Docs](/installation)
[Github](https://github.com/daylinmorgan/viv)
:::
`Viv` is a standalone dependency-free `venv` creator (just needs python + pip).
`Viv` helps you ignore silly things like managing temporary or rarely used virtual environments,
while still unleashing the full power of python scripting with it's entire ecosystem at your disposal.
`Viv`'s uncompromising insistence on portability means that it will always:
1. only use the standard library
2. never exceed a single script.
`Viv`'s uncompromising insistence on portability means that it will always,
only use the standard library and never exceed a single script.
Documentation is currently a WIP please see the [cli reference](./cli.md) and the [README](https://github.com/daylinmorgan/viv)
```{toctree}
:hidden:
:maxdepth: 2
installation.md
usage.md
configuration.md
vs-others.md
cli.md
```

56
docs/installation.md Normal file
View File

@ -0,0 +1,56 @@
# Installation
## Automated
Run the below command to install `viv`.
```sh
python3 <(curl -fsSL viv.dayl.in/viv.py) manage install
```
To access `viv` from within scripts you should add its location to your `PYTHONPATH`.
By default `viv` will be installed to `$XDG_DATA_HOME/viv` or `~/.local/share/viv`,
and symlinked to `$XDG_BIN_HOME` or `~/.local/bin/.`
You can customize these locations at install with with `--src` and `--cli` respectively.
```sh
export PYTHONPATH="$PYTHONPATH:$HOME/.local/share/viv"
# or
export PYTHONPATH="$PYTHONPATH:$(viv manage show --pythonpath)"
```
:::{note}
You can install an older version by specifying `--ref`
:::
## Manual
Viv is a single standalone script meaning all that's really necessary is
that it exists locally and is appropriately added to your path.
You can get the latest stable version from [here](https://viv.dayl.in/viv.py)
or from [github](https://github.com/daylinmorgan/viv/blob/latest/src/viv/viv.py).
Must be added to your `$PATH` for use as a CLI app and `$PYTHONPATH` for uses as a python module.
## Never
Viv is a standalone script accessible at `https://viv.dayl.in/viv.py`
meaning it's not strictly necessary you every actually install it.
Every instance of the `viv` CLI can be seamlessly replaced with `python3 <(curl -fsSL viv.dayl.in/viv.py)`.
## PyPI Options (Not Recommended)
Why is this *not recommended?*
Mainly because `viv` is all about hacking your `sys.path`.
Placing it in its own virtual environment
or installing in a user site directory may complicate this endeavor.
```sh
pip install viv
# or
pipx install viv
```
*Note*: If installed by `pipx`, it should still be possible to
use `viv` as a module by adding it to the `$PYTHONPATH`.
See above for more info.

View File

@ -1,12 +1,181 @@
# Usage
In any python script with external dependencies you can add this line,
Viv works by ensuring scripts or a given
command are run in an appropriate environment with all specified dependencies.
```{tip}
`viv` is a single script and available at viv.dayl.in/viv.py
meaning every instance of `viv` in these examples could be `python3 <(curl -fsSL viv.dayl.in/viv.py)`
```
## Run CLI Apps
Run a python app that provides an entrypoint and separate args with `--`:
```sh
viv run frogmouth -- gh daylinmorgan/viv
```
To run a python module use the `-b/--bin` flag and specify `python`:
```sh
viv run rich -b python -- -m rich
```
Make an ephemeral `jupyter` environment with your needed deps:
```sh
viv run jupyter pandas -r requirements-dev.txt -- notebook
```
Generate an executable shell script that will on demand create a vivenv as needed:
```sh
viv shim ruff
```
Output:
```python
#!/usr/bin/env python3
# AUTOGENERATED by viv (v2023.1003)
# see `python3 <(curl -fsSL viv.dayl.in/viv.py) --help`
import subprocess
import sys
if __name__ == "__main__":
vivenv = __import__("viv").use("ruff") # noqa
sys.exit(subprocess.run([vivenv / "bin" / "ruff", *sys.argv[1:]]).returncode)
```
## Run Python Scripts
It's possible to use the `viv` CLI to run a python script.
There are several options for invoking a script.
Using the interpreter:
```sh
viv run rich typer -b python -- ./cli.py --help
```
Using `-s/--script`:
```sh
viv run rich typer -s ./cli.py -- --help
# or with a remote script
viv run rich -s https://raw.githubusercontent.com/Textualize/rich/master/examples/fullscreen.py
```
If `viv` is available on your path it's possible to
invoke it with embedded metadata thanks to shebangs:
```python
#!/usr/bin/env -S viv run --keep --script
# /// script
# requires-python = ">3.10"
# dependencies = [
# "matplotlib",
# "pandas"
# ]
# ///
```
```{note}
If using a shebang on a python script `-s/--script` must be the last argument
```
```{seealso}
Check out [PEP723](https://peps.python.org/pep-0723/) for more info about inline script metadata.
```
In any python script with external dependencies you can also add this line prior to imports
to automate `vivenv` creation and installation of dependencies.
```python
__import__("viv").use("click")
```
If your dependencies are sensitive to the version of python
(numpy, cpython-based apps, etc. ) then you can specify `track_exe`.
Which will lead to `viv` creating a unique vivenv based on the detected python executable.
```python
__import__("viv").use("numpy", track_exe=True)
```
If you'd like to pin your dependencies to a resolved environment you
can use the convenience command `viv freeze` to output a list of pinned packages.
Command:
```sh
viv freeze rich typer
```
Output:
```python
__import__("viv").use("rich==13.7.0", "typer==0.9.0", "click==8.1.7", "markdown-it-py==3.0.0", "Pygments==2.17.2", "typing_extensions==4.9.0", "mdurl==0.1.2") # noqa
```
Additionally, you can make this work regardless of `PYTHONPATH` by using `--path`.
Command:
```sh
viv freeze rich typer --path rel
```
Output:
```python
__import__("sys").path.append(__import__("os").path.expanduser("~/.local/share/viv/")) # noqa
__import__("viv").use("rich==13.7.0", "typer==0.9.0", "click==8.1.7", "markdown-it-py==3.0.0", "Pygments==2.17.2", "typing_extensions==4.9.0", "mdurl==0.1.2") # noqa
```
## Manage Viv
### Interacting with the viv cache
Depending on how you invoke `viv`, it will persist it's virtual environments (vivenvs).
To see all currently existing vivenvs use can use `viv list`.
Viv will attempt to track any usages of the
vivenvs including the scripts that invoke them.
You can remove any existing vivenvs using `viv env remove`:
```sh
viv env remove d4b342b3
```
To get more information about vivenvs you can use `viv list --verbose` or `viv env info <hash>`
```{note}
For commands that expect a vivenv hash/name you can use as few characters as you
as you like and `viv` will match it against the existing vivenvs in the cache.
```
You can list vivenvs given a criteria using `--filter` for example:
Created before 2024-01-01:
```sh
viv list --filter "created-before:2024-01-01"
```
Associated with a particular file:
```sh
viv list --filter "files:./script.py"
```
Or no files:
```sh
viv list --filter "files:None"
```
```{note}
`--filter "files:None"` will also apply to vivenvs
in which the original file is no longer on the disk
```
The available filtering criteria are `accessed-after`,
`accessed-before`, `created-before`, `created-after`, and `files`.
To remove all `vivenvs` you can use the below command:
```sh
@ -18,35 +187,8 @@ To remove `viv` all together you can use the included `purge` command:
```sh
python3 <(curl -fsSL viv.dayl.in/viv.py) manage purge
```
## Equivalent commands from alternatives
### [pip-run](https://github.com/jaraco/pip-run)
```sh
pip-run cowsay -- -m cowsay "moove over, pip-run"
python3 <(curl -fsSL viv.dayl.in/viv.py) run cowsay -- "moove over, pip-run"
```
```sh
python -m pip-run requests -- -c "import requests; print(requests.get('https://pypi.org/project/pip-run').status_code)"
python -m viv run requests -b python -- -c "import requests; print(requests.get('https://pypi.org/project/viv').status_code)"
```
### [pipx](https://github.com/pypa/pipx/)
```sh
pipx install pycowsay
viv shim pycowsay
```
```sh
pipx run https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py
python3 <(curl -fsSL viv.dayl.in/viv.py) run \
-s https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py
```
## Bonus: use `viv` with just standalone snippet (37LOC)
## Bonus: `viv` standalone
`--standalone` will auto-generate a mini function version of `viv` to accomplish the same basic task as using a local copy of `viv`.
After generating this standalone `shim` you can freely use this script across unix machines which have `python>3.8`.
@ -58,16 +200,3 @@ See [examples/black](https://github.com/daylinmorgan/viv/blob/dev/examples/black
python3 <(curl -fsSL viv.dayl.in/viv.py) shim black -o ./black --standalone --freeze
```
[^1]: You do need to have `pip` but surely you have `pip` already.
[conda-shield]: https://img.shields.io/conda/vn/conda-forge/viv
[conda-url]: https://anaconda.org/conda-forge/viv
[pypi-shield]: https://img.shields.io/pypi/v/viv
[pypi-url]: https://pypi.org/project/viv
[stars-shield]: https://img.shields.io/github/stars/daylinmorgan/viv.svg
[stars-url]: https://github.com/daylinmorgan/viv/stargazers
[issues-shield]: https://img.shields.io/github/issues/daylinmorgan/viv.svg
[issues-url]: https://github.com/daylinmorgan/viv/issues
[license-shield]: https://img.shields.io/github/license/daylinmorgan/viv.svg
[license-url]: https://github.com/daylinmorgan/viv/blob/main/LICENSE

31
docs/vs-others.md Normal file
View File

@ -0,0 +1,31 @@
# Vs Others
## Equivalent commands from alternatives
### [pip-run](https://github.com/jaraco/pip-run)
```sh
pip-run cowsay -- -m cowsay "moove over, pip-run"
python3 <(curl -fsSL viv.dayl.in/viv.py) run cowsay -- "moove over, pip-run"
```
```sh
python -m pip-run requests -- -c "import requests; print(requests.get('https://pypi.org/project/pip-run').status_code)"
python -m viv run requests -b python -- -c "import requests; print(requests.get('https://pypi.org/project/viv').status_code)"
```
### [pipx](https://github.com/pypa/pipx/)
```sh
pipx install pycowsay
viv shim pycowsay
```
```sh
pipx run https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py
python3 <(curl -fsSL viv.dayl.in/viv.py) run \
-s https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py
```

106
pdm.lock
View File

@ -5,7 +5,7 @@
groups = ["default", "dev", "docs", "test"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:67b99f093f433bb1a56dc393d51c615b65e94ea9ad397a5460c5a2a29a21cb22"
content_hash = "sha256:527163383ae4b3489c9082bc814dacd347ee94ad6615c214ba63496eb1bdad2a"
[[package]]
name = "alabaster"
@ -50,16 +50,6 @@ files = [
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
name = "cfgv"
version = "3.4.0"
requires_python = ">=3.8"
summary = "Validate configuration and produce human readable error messages."
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
@ -155,15 +145,6 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.3.8"
summary = "Distribution utilities"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "docutils"
version = "0.20.1"
@ -184,26 +165,6 @@ files = [
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[[package]]
name = "filelock"
version = "3.13.1"
requires_python = ">=3.8"
summary = "A platform independent file lock."
files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
]
[[package]]
name = "identify"
version = "2.5.33"
requires_python = ">=3.8"
summary = "File identification library for Python"
files = [
{file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
{file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
]
[[package]]
name = "idna"
version = "3.6"
@ -436,19 +397,6 @@ files = [
{file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"},
]
[[package]]
name = "nodeenv"
version = "1.8.0"
requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
summary = "Node.js virtual environment builder"
dependencies = [
"setuptools",
]
files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
[[package]]
name = "packaging"
version = "23.2"
@ -468,16 +416,6 @@ files = [
{file = "peppercorn-0.6.tar.gz", hash = "sha256:96d7681d7a04545cfbaf2c6fb66de67b29cfc42421aa263e4c78f2cbb85be4c6"},
]
[[package]]
name = "platformdirs"
version = "4.1.0"
requires_python = ">=3.8"
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
files = [
{file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
{file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
]
[[package]]
name = "pluggy"
version = "1.3.0"
@ -488,23 +426,6 @@ files = [
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[[package]]
name = "pre-commit"
version = "3.5.0"
requires_python = ">=3.8"
summary = "A framework for managing and maintaining multi-language pre-commit hooks."
dependencies = [
"cfgv>=2.0.0",
"identify>=1.0.0",
"nodeenv>=0.11.1",
"pyyaml>=5.1",
"virtualenv>=20.10.0",
]
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[[package]]
name = "pygments"
version = "2.17.2"
@ -632,16 +553,6 @@ files = [
{file = "sampleproject-3.0.0.tar.gz", hash = "sha256:117ed88e5db073bb92969a7545745fd977ee85b7019706dd256a64058f70963d"},
]
[[package]]
name = "setuptools"
version = "69.0.3"
requires_python = ">=3.8"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
files = [
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
]
[[package]]
name = "shibuya"
version = "2024.1.2"
@ -840,21 +751,6 @@ files = [
{file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
]
[[package]]
name = "virtualenv"
version = "20.25.0"
requires_python = ">=3.7"
summary = "Virtual Python Environment builder"
dependencies = [
"distlib<1,>=0.3.7",
"filelock<4,>=3.12.2",
"platformdirs<5,>=3.9.1",
]
files = [
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
]
[[package]]
name = "yartsu"
version = "23.5.1b1"

View File

@ -21,20 +21,21 @@ viv = "viv:main"
[tool.pdm]
version = { source = "scm" }
# need python 3.9 for these which I usually have anyways
ignore_package_warnings = ["sphinx*", "myst-parser"]
[tool.pdm.dev-dependencies]
dev = [
"pre-commit>=3",
"mypy>=0.991",
"astor>=0.8.1",
]
docs = [
"sphinx",
"sphinx-autobuild",
"sphinx-copybutton",
"myst-parser",
"shibuya",
"yartsu"
"sphinx",
"sphinx-autobuild",
"sphinx-copybutton",
"myst-parser",
"shibuya",
"yartsu",
]
test = [
"pytest",

View File

@ -33,6 +33,7 @@ from argparse import (
from argparse import ArgumentParser as StdArgParser
from contextlib import contextmanager
from datetime import datetime
from enum import Enum
from logging.handlers import RotatingFileHandler
from pathlib import Path
from textwrap import dedent, fill
@ -53,7 +54,7 @@ from typing import (
Union,
)
__version__ = "2023.1003-pep723"
__version__ = "2024.1004"
#### START VENDORED TOMLI ####
@ -2864,41 +2865,43 @@ def make_executable(path: Path) -> None:
os.chmod(path, mode)
# TODO: abstract/deduplicate these functions
def _uses_viv_use(txt: str) -> bool:
return bool(
re.search(
class _Viv_Mode(Enum):
NONE = 0
USE = 1
RUN = 2
def _uses_viv(txt: str) -> _Viv_Mode:
matches = [
match.group("mode")
for match in re.finditer(
r"""
^(?!\#)\s*
(?:__import__\(\s*["']viv["']\s*\).use)
|
(?:from\ viv\ import\ use)
#|
#(?:import\ viv)
|
(?:viv.use)
""",
^(?!\#)\s* # ignore comments/shebangs
(
(?:__import__\(\s*["']viv["']\s*\)\.)
|
(?:from\s+viv\s+import\s+)
|
(?:viv\.)
)
(?P<mode>(\w+))
""",
txt,
re.VERBOSE | re.MULTILINE,
)
)
def _uses_viv_run(txt: str) -> bool:
return bool(
re.search(
r"""
^(?!\#)\s*
(?:__import__\(\s*["']viv["']\s*\).run)
|
(?:from\ viv\ import\ run)
|
(?:viv.run)
""",
txt,
re.VERBOSE | re.MULTILINE,
]
if len(matches) == 0:
return _Viv_Mode.NONE
elif len(matches) > 1:
err_quit(
"Unexpected number of viv references in script.\n"
"Expected only 1, found: "
+ ", ".join((a.style(match, "bold") for match in matches))
)
)
elif (match := matches[0]) in {"run", "use"}:
return _Viv_Mode[match.upper()]
else:
err_quit(f"Unknown function {a.bold}{matches[0]}{a.end} associated with viv.")
METADATA_BLOCK = (
@ -3437,21 +3440,20 @@ class Viv:
script_text = fetch_script(script)
scriptpath.write_text(script_text)
has_use = _uses_viv_use(script_text)
has_run = _uses_viv_run(script_text)
mode = _uses_viv(script_text)
metadata = _read_metadata_block(script_text)
deps = metadata.get("dependencies", [])
if requires := metadata.get("requires-python", ""):
_check_python(requires)
if has_use and deps:
if mode == _Viv_Mode.USE and deps:
error(
"Script Dependencies block and "
"`viv.use` API can't be used in the same script"
)
if not self.local_source and (has_use or has_run):
if not self.local_source and mode != _Viv_Mode.NONE:
log.debug("fetching remote copy to use for python api")
(tmppath / "viv.py").write_text(
fetch_script(
@ -3461,7 +3463,7 @@ class Viv:
self._update_cache(env, keep, tmpdir)
if has_use:
if mode == _Viv_Mode.USE:
log.debug(f"script invokes viv.use passing along spec: \n '{spec}'")
subprocess_run_quit(
[sys.executable, "-S", scriptpath, *rest],
@ -3471,9 +3473,8 @@ class Viv:
PYTHONPATH=":".join((str(tmppath), env.get("PYTHONPATH", ""))),
),
)
elif has_run:
elif mode == _Viv_Mode.RUN:
log.debug("script invokes viv.run letting subprocess handle deps")
print(tmppath)
subprocess_run_quit(
[sys.executable, "-S", scriptpath, *rest],
env=dict(

52
tests/test_readers.py Normal file
View File

@ -0,0 +1,52 @@
import pytest
from viv.viv import _read_metadata_block, _uses_viv, _Viv_Mode
RUN_METADATA_SCRIPT = """
#!/usr/bin/env -S viv run -s
# /// script
# requires-python = ">3.10"
# dependencies = [
# "rich"
# ]
# ///
"""
USE_SCRIPT = """
#!/usr/bin/env python3
__import__("viv").use("rich")
from rich import print
print("pretty!")
"""
def test_metadata():
metadata = _read_metadata_block(RUN_METADATA_SCRIPT)
assert metadata == {"requires-python": ">3.10", "dependencies": ["rich"]}
def test_uses():
assert _uses_viv(RUN_METADATA_SCRIPT) == _Viv_Mode.NONE
assert (
_uses_viv(RUN_METADATA_SCRIPT + """\n__import__("viv").run()\n""")
== _Viv_Mode.RUN
)
assert _uses_viv(USE_SCRIPT) == _Viv_Mode.USE
assert _uses_viv("# from viv import use") == _Viv_Mode.NONE
def test_uses_fail(caplog):
with pytest.raises(SystemExit):
_uses_viv("""__import__("viv").run()\n__import__("viv").use()""")
with pytest.raises(SystemExit):
_uses_viv("""__import__("viv").unknown()""")
assert [
(
"viv",
40,
"Unexpected number of viv references in script.\n"
"Expected only 1, found: run, use",
),
("viv", 40, "Unknown function unknown associated with viv."),
] == caplog.record_tuples