mirror of https://github.com/daylinmorgan/viv.git
3889 lines
127 KiB
Python
Executable File
3889 lines
127 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""viv isn't venv!
|
|
|
|
viv -h
|
|
OR
|
|
__import__("viv").use("requests", "bs4")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import itertools
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import site
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import venv
|
|
from argparse import (
|
|
SUPPRESS,
|
|
Action,
|
|
HelpFormatter,
|
|
Namespace,
|
|
RawDescriptionHelpFormatter,
|
|
_SubParsersAction,
|
|
)
|
|
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
|
|
from time import sleep
|
|
from types import TracebackType
|
|
from typing import (
|
|
Any,
|
|
Dict,
|
|
Generator,
|
|
List,
|
|
NoReturn,
|
|
Optional,
|
|
Sequence,
|
|
Set,
|
|
TextIO,
|
|
Tuple,
|
|
Type,
|
|
Union,
|
|
)
|
|
|
|
__version__ = "2023.1003-pep723"
|
|
|
|
|
|
#### START VENDORED TOMLI ####
|
|
|
|
if sys.version_info >= (3, 11):
|
|
from tomllib import loads as toml_loads
|
|
else:
|
|
# MODIFIED FROM https://github.com/hukkin/tomli
|
|
# see below for original license
|
|
# SPDX-License-Identifier: MIT
|
|
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
# Licensed to PSF under a Contributor Agreement.
|
|
|
|
import string # noqa
|
|
from collections.abc import Iterable # noqa
|
|
from functools import lru_cache # noqa
|
|
from datetime import date, datetime, time, timedelta, timezone, tzinfo # noqa
|
|
from types import MappingProxyType # noqa
|
|
from typing import IO, Any, Callable, NamedTuple # noqa
|
|
|
|
v_tomli_ParseFloat = Callable[[str], Any]
|
|
Key = Tuple[str, ...]
|
|
v_tomli_Pos = int
|
|
v_tomli__TIME_RE_STR = (
|
|
"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\\.([0-9]{1,6})[0-9]*)?"
|
|
)
|
|
v_tomli_RE_NUMBER = re.compile(
|
|
"""
|
|
0
|
|
(?:
|
|
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
|
|
|
|
b[01](?:_?[01])* # bin
|
|
|
|
|
o[0-7](?:_?[0-7])* # oct
|
|
)
|
|
|
|
|
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
|
(?P<floatpart>
|
|
(?:\\.[0-9](?:_?[0-9])*)? # optional fractional part
|
|
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
|
)
|
|
""",
|
|
flags=re.VERBOSE,
|
|
)
|
|
v_tomli_RE_LOCALTIME = re.compile(v_tomli__TIME_RE_STR)
|
|
v_tomli_RE_DATETIME = re.compile(
|
|
f"""
|
|
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
|
(?:
|
|
[Tt ]
|
|
{v_tomli__TIME_RE_STR}
|
|
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
|
)?
|
|
""",
|
|
flags=re.VERBOSE,
|
|
)
|
|
|
|
def v_tomli_match_to_datetime(match: re.Match) -> datetime | date:
|
|
(
|
|
year_str,
|
|
month_str,
|
|
day_str,
|
|
hour_str,
|
|
minute_str,
|
|
sec_str,
|
|
micros_str,
|
|
zulu_time,
|
|
offset_sign_str,
|
|
offset_hour_str,
|
|
offset_minute_str,
|
|
) = match.groups()
|
|
year, month, day = int(year_str), int(month_str), int(day_str)
|
|
if hour_str is None:
|
|
return date(year, month, day)
|
|
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
|
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
if offset_sign_str:
|
|
tz: tzinfo | None = v_tomli_cached_tz(
|
|
offset_hour_str, offset_minute_str, offset_sign_str
|
|
)
|
|
elif zulu_time:
|
|
tz = timezone.utc
|
|
else:
|
|
tz = None
|
|
return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def v_tomli_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
|
|
sign = 1 if sign_str == "+" else -1
|
|
return timezone(
|
|
timedelta(hours=sign * int(hour_str), minutes=sign * int(minute_str))
|
|
)
|
|
|
|
def v_tomli_match_to_localtime(match: re.Match) -> time:
|
|
hour_str, minute_str, sec_str, micros_str = match.groups()
|
|
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
return time(int(hour_str), int(minute_str), int(sec_str), micros)
|
|
|
|
def v_tomli_match_to_number(
|
|
match: re.Match, parse_float: v_tomli_ParseFloat
|
|
) -> Any:
|
|
if match.group("floatpart"):
|
|
return parse_float(match.group())
|
|
return int(match.group(), 0)
|
|
|
|
v_tomli_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
|
|
v_tomli_ILLEGAL_BASIC_STR_CHARS = v_tomli_ASCII_CTRL - frozenset("\t")
|
|
v_tomli_ILLEGAL_MULTILINE_BASIC_STR_CHARS = v_tomli_ASCII_CTRL - frozenset("\t\n")
|
|
v_tomli_ILLEGAL_LITERAL_STR_CHARS = v_tomli_ILLEGAL_BASIC_STR_CHARS
|
|
v_tomli_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = (
|
|
v_tomli_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
|
)
|
|
v_tomli_ILLEGAL_COMMENT_CHARS = v_tomli_ILLEGAL_BASIC_STR_CHARS
|
|
v_tomli_TOML_WS = frozenset(" \t")
|
|
v_tomli_TOML_WS_AND_NEWLINE = v_tomli_TOML_WS | frozenset("\n")
|
|
v_tomli_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
|
|
v_tomli_KEY_INITIAL_CHARS = v_tomli_BARE_KEY_CHARS | frozenset("\"'")
|
|
v_tomli_HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
|
v_tomli_BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
|
|
{
|
|
"\\b": "\x08",
|
|
"\\t": "\t",
|
|
"\\n": "\n",
|
|
"\\f": "\x0c",
|
|
"\\r": "\r",
|
|
'\\"': '"',
|
|
"\\\\": "\\",
|
|
}
|
|
)
|
|
|
|
class v_tomli_TOMLDecodeError(ValueError):
|
|
pass
|
|
|
|
def v_tomli_load(
|
|
__fp: IO[bytes], *, parse_float: v_tomli_ParseFloat = float
|
|
) -> dict[str, Any]:
|
|
b = __fp.read()
|
|
try:
|
|
s = b.decode()
|
|
except AttributeError:
|
|
raise TypeError(
|
|
"File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
|
|
) from None
|
|
return v_tomli_loads(s, parse_float=parse_float)
|
|
|
|
def v_tomli_loads(
|
|
__s: str, *, parse_float: v_tomli_ParseFloat = float
|
|
) -> dict[str, Any]:
|
|
src = __s.replace("\r\n", "\n")
|
|
pos = 0
|
|
out = v_tomli_Output(v_tomli_NestedDict(), v_tomli_Flags())
|
|
header: Key = ()
|
|
parse_float = v_tomli_make_safe_parse_float(parse_float)
|
|
while True:
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
try:
|
|
char = src[pos]
|
|
except IndexError:
|
|
break
|
|
if char == "\n":
|
|
pos += 1
|
|
continue
|
|
if char in v_tomli_KEY_INITIAL_CHARS:
|
|
pos = v_tomli_key_value_rule(src, pos, out, header, parse_float)
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
elif char == "[":
|
|
try:
|
|
second_char: str | None = src[pos + 1]
|
|
except IndexError:
|
|
second_char = None
|
|
out.flags.finalize_pending()
|
|
if second_char == "[":
|
|
pos, header = v_tomli_create_list_rule(src, pos, out)
|
|
else:
|
|
pos, header = v_tomli_create_dict_rule(src, pos, out)
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
elif char != "#":
|
|
raise v_tomli_suffixed_err(src, pos, "Invalid statement")
|
|
pos = v_tomli_skip_comment(src, pos)
|
|
try:
|
|
char = src[pos]
|
|
except IndexError:
|
|
break
|
|
if char != "\n":
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Expected newline or end of document after a statement"
|
|
)
|
|
pos += 1
|
|
return out.data.dict
|
|
|
|
class v_tomli_Flags:
|
|
FROZEN = 0
|
|
EXPLICIT_NEST = 1
|
|
|
|
def __init__(self) -> None:
|
|
self._flags: dict[str, dict] = {}
|
|
self._pending_flags: set[tuple[Key, int]] = set()
|
|
|
|
def add_pending(self, key: Key, flag: int) -> None:
|
|
self._pending_flags.add((key, flag))
|
|
|
|
def finalize_pending(self) -> None:
|
|
for key, flag in self._pending_flags:
|
|
self.set(key, flag, recursive=False)
|
|
self._pending_flags.clear()
|
|
|
|
def unset_all(self, key: Key) -> None:
|
|
cont = self._flags
|
|
for k in key[:-1]:
|
|
if k not in cont:
|
|
return
|
|
cont = cont[k]["nested"]
|
|
cont.pop(key[-1], None)
|
|
|
|
def set(self, key: Key, flag: int, *, recursive: bool) -> None:
|
|
cont = self._flags
|
|
key_parent, key_stem = key[:-1], key[-1]
|
|
for k in key_parent:
|
|
if k not in cont:
|
|
cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
|
|
cont = cont[k]["nested"]
|
|
if key_stem not in cont:
|
|
cont[key_stem] = {
|
|
"flags": set(),
|
|
"recursive_flags": set(),
|
|
"nested": {},
|
|
}
|
|
cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)
|
|
|
|
def is_(self, key: Key, flag: int) -> bool:
|
|
if not key:
|
|
return False
|
|
cont = self._flags
|
|
for k in key[:-1]:
|
|
if k not in cont:
|
|
return False
|
|
inner_cont = cont[k]
|
|
if flag in inner_cont["recursive_flags"]:
|
|
return True
|
|
cont = inner_cont["nested"]
|
|
key_stem = key[-1]
|
|
if key_stem in cont:
|
|
cont = cont[key_stem]
|
|
return flag in cont["flags"] or flag in cont["recursive_flags"]
|
|
return False
|
|
|
|
class v_tomli_NestedDict:
|
|
def __init__(self) -> None:
|
|
self.dict: dict[str, Any] = {}
|
|
|
|
def get_or_create_nest(self, key: Key, *, access_lists: bool = True) -> dict:
|
|
cont: Any = self.dict
|
|
for k in key:
|
|
if k not in cont:
|
|
cont[k] = {}
|
|
cont = cont[k]
|
|
if access_lists and isinstance(cont, list):
|
|
cont = cont[-1]
|
|
if not isinstance(cont, dict):
|
|
raise KeyError("There is no nest behind this key")
|
|
return cont
|
|
|
|
def append_nest_to_list(self, key: Key) -> None:
|
|
cont = self.get_or_create_nest(key[:-1])
|
|
last_key = key[-1]
|
|
if last_key in cont:
|
|
list_ = cont[last_key]
|
|
if not isinstance(list_, list):
|
|
raise KeyError("An object other than list found behind this key")
|
|
list_.append({})
|
|
else:
|
|
cont[last_key] = [{}]
|
|
|
|
class v_tomli_Output(NamedTuple):
|
|
data: v_tomli_NestedDict
|
|
flags: v_tomli_Flags
|
|
|
|
def v_tomli_skip_chars(
|
|
src: str, pos: v_tomli_Pos, chars: Iterable[str]
|
|
) -> v_tomli_Pos:
|
|
try:
|
|
while src[pos] in chars:
|
|
pos += 1
|
|
except IndexError:
|
|
pass
|
|
return pos
|
|
|
|
def v_tomli_skip_until(
|
|
src: str,
|
|
pos: v_tomli_Pos,
|
|
expect: str,
|
|
*,
|
|
error_on: frozenset[str],
|
|
error_on_eof: bool,
|
|
) -> v_tomli_Pos:
|
|
try:
|
|
new_pos = src.index(expect, pos)
|
|
except ValueError:
|
|
new_pos = len(src)
|
|
if error_on_eof:
|
|
raise v_tomli_suffixed_err(
|
|
src, new_pos, f"Expected {expect!r}"
|
|
) from None
|
|
if not error_on.isdisjoint(src[pos:new_pos]):
|
|
while src[pos] not in error_on:
|
|
pos += 1
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Found invalid character {src[pos]!r}"
|
|
)
|
|
return new_pos
|
|
|
|
def v_tomli_skip_comment(src: str, pos: v_tomli_Pos) -> v_tomli_Pos:
|
|
try:
|
|
char: str | None = src[pos]
|
|
except IndexError:
|
|
char = None
|
|
if char == "#":
|
|
return v_tomli_skip_until(
|
|
src,
|
|
pos + 1,
|
|
"\n",
|
|
error_on=v_tomli_ILLEGAL_COMMENT_CHARS,
|
|
error_on_eof=False,
|
|
)
|
|
return pos
|
|
|
|
def v_tomli_skip_comments_and_array_ws(src: str, pos: v_tomli_Pos) -> v_tomli_Pos:
|
|
while True:
|
|
pos_before_skip = pos
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS_AND_NEWLINE)
|
|
pos = v_tomli_skip_comment(src, pos)
|
|
if pos == pos_before_skip:
|
|
return pos
|
|
|
|
def v_tomli_create_dict_rule(
|
|
src: str, pos: v_tomli_Pos, out: v_tomli_Output
|
|
) -> tuple[v_tomli_Pos, Key]:
|
|
pos += 1
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
pos, key = v_tomli_parse_key(src, pos)
|
|
if out.flags.is_(key, v_tomli_Flags.EXPLICIT_NEST) or out.flags.is_(
|
|
key, v_tomli_Flags.FROZEN
|
|
):
|
|
raise v_tomli_suffixed_err(src, pos, f"Cannot declare {key} twice")
|
|
out.flags.set(key, v_tomli_Flags.EXPLICIT_NEST, recursive=False)
|
|
try:
|
|
out.data.get_or_create_nest(key)
|
|
except KeyError:
|
|
raise v_tomli_suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
if not src.startswith("]", pos):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Expected ']' at the end of a table declaration"
|
|
)
|
|
return pos + 1, key
|
|
|
|
def v_tomli_create_list_rule(
|
|
src: str, pos: v_tomli_Pos, out: v_tomli_Output
|
|
) -> tuple[v_tomli_Pos, Key]:
|
|
pos += 2
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
pos, key = v_tomli_parse_key(src, pos)
|
|
if out.flags.is_(key, v_tomli_Flags.FROZEN):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Cannot mutate immutable namespace {key}"
|
|
)
|
|
out.flags.unset_all(key)
|
|
out.flags.set(key, v_tomli_Flags.EXPLICIT_NEST, recursive=False)
|
|
try:
|
|
out.data.append_nest_to_list(key)
|
|
except KeyError:
|
|
raise v_tomli_suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
if not src.startswith("]]", pos):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Expected ']]' at the end of an array declaration"
|
|
)
|
|
return pos + 2, key
|
|
|
|
def v_tomli_key_value_rule(
|
|
src: str,
|
|
pos: v_tomli_Pos,
|
|
out: v_tomli_Output,
|
|
header: Key,
|
|
parse_float: v_tomli_ParseFloat,
|
|
) -> v_tomli_Pos:
|
|
pos, key, value = v_tomli_parse_key_value_pair(src, pos, parse_float)
|
|
key_parent, key_stem = key[:-1], key[-1]
|
|
abs_key_parent = header + key_parent
|
|
relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
|
|
for cont_key in relative_path_cont_keys:
|
|
if out.flags.is_(cont_key, v_tomli_Flags.EXPLICIT_NEST):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Cannot redefine namespace {cont_key}"
|
|
)
|
|
out.flags.add_pending(cont_key, v_tomli_Flags.EXPLICIT_NEST)
|
|
if out.flags.is_(abs_key_parent, v_tomli_Flags.FROZEN):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Cannot mutate immutable namespace {abs_key_parent}"
|
|
)
|
|
try:
|
|
nest = out.data.get_or_create_nest(abs_key_parent)
|
|
except KeyError:
|
|
raise v_tomli_suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
if key_stem in nest:
|
|
raise v_tomli_suffixed_err(src, pos, "Cannot overwrite a value")
|
|
if isinstance(value, (dict, list)):
|
|
out.flags.set(header + key, v_tomli_Flags.FROZEN, recursive=True)
|
|
nest[key_stem] = value
|
|
return pos
|
|
|
|
def v_tomli_parse_key_value_pair(
|
|
src: str, pos: v_tomli_Pos, parse_float: v_tomli_ParseFloat
|
|
) -> tuple[v_tomli_Pos, Key, Any]:
|
|
pos, key = v_tomli_parse_key(src, pos)
|
|
try:
|
|
char: str | None = src[pos]
|
|
except IndexError:
|
|
char = None
|
|
if char != "=":
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Expected '=' after a key in a key/value pair"
|
|
)
|
|
pos += 1
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
pos, value = v_tomli_parse_value(src, pos, parse_float)
|
|
return pos, key, value
|
|
|
|
def v_tomli_parse_key(src: str, pos: v_tomli_Pos) -> tuple[v_tomli_Pos, Key]:
|
|
pos, key_part = v_tomli_parse_key_part(src, pos)
|
|
key: Key = (key_part,)
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
while True:
|
|
try:
|
|
char: str | None = src[pos]
|
|
except IndexError:
|
|
char = None
|
|
if char != ".":
|
|
return pos, key
|
|
pos += 1
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
pos, key_part = v_tomli_parse_key_part(src, pos)
|
|
key += (key_part,)
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
|
|
def v_tomli_parse_key_part(src: str, pos: v_tomli_Pos) -> tuple[v_tomli_Pos, str]:
|
|
try:
|
|
char: str | None = src[pos]
|
|
except IndexError:
|
|
char = None
|
|
if char in v_tomli_BARE_KEY_CHARS:
|
|
start_pos = pos
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_BARE_KEY_CHARS)
|
|
return pos, src[start_pos:pos]
|
|
if char == "'":
|
|
return v_tomli_parse_literal_str(src, pos)
|
|
if char == '"':
|
|
return v_tomli_parse_one_line_basic_str(src, pos)
|
|
raise v_tomli_suffixed_err(src, pos, "Invalid initial character for a key part")
|
|
|
|
def v_tomli_parse_one_line_basic_str(
|
|
src: str, pos: v_tomli_Pos
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
pos += 1
|
|
return v_tomli_parse_basic_str(src, pos, multiline=False)
|
|
|
|
def v_tomli_parse_array(
|
|
src: str, pos: v_tomli_Pos, parse_float: v_tomli_ParseFloat
|
|
) -> tuple[v_tomli_Pos, list]:
|
|
pos += 1
|
|
array: list = []
|
|
pos = v_tomli_skip_comments_and_array_ws(src, pos)
|
|
if src.startswith("]", pos):
|
|
return pos + 1, array
|
|
while True:
|
|
pos, val = v_tomli_parse_value(src, pos, parse_float)
|
|
array.append(val)
|
|
pos = v_tomli_skip_comments_and_array_ws(src, pos)
|
|
c = src[pos : pos + 1]
|
|
if c == "]":
|
|
return pos + 1, array
|
|
if c != ",":
|
|
raise v_tomli_suffixed_err(src, pos, "Unclosed array")
|
|
pos += 1
|
|
pos = v_tomli_skip_comments_and_array_ws(src, pos)
|
|
if src.startswith("]", pos):
|
|
return pos + 1, array
|
|
|
|
def v_tomli_parse_inline_table(
|
|
src: str, pos: v_tomli_Pos, parse_float: v_tomli_ParseFloat
|
|
) -> tuple[v_tomli_Pos, dict]:
|
|
pos += 1
|
|
nested_dict = v_tomli_NestedDict()
|
|
flags = v_tomli_Flags()
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
if src.startswith("}", pos):
|
|
return pos + 1, nested_dict.dict
|
|
while True:
|
|
pos, key, value = v_tomli_parse_key_value_pair(src, pos, parse_float)
|
|
key_parent, key_stem = key[:-1], key[-1]
|
|
if flags.is_(key, v_tomli_Flags.FROZEN):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Cannot mutate immutable namespace {key}"
|
|
)
|
|
try:
|
|
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
|
except KeyError:
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Cannot overwrite a value"
|
|
) from None
|
|
if key_stem in nest:
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, f"Duplicate inline table key {key_stem!r}"
|
|
)
|
|
nest[key_stem] = value
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
c = src[pos : pos + 1]
|
|
if c == "}":
|
|
return pos + 1, nested_dict.dict
|
|
if c != ",":
|
|
raise v_tomli_suffixed_err(src, pos, "Unclosed inline table")
|
|
if isinstance(value, (dict, list)):
|
|
flags.set(key, v_tomli_Flags.FROZEN, recursive=True)
|
|
pos += 1
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
|
|
def v_tomli_parse_basic_str_escape(
|
|
src: str, pos: v_tomli_Pos, *, multiline: bool = False
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
escape_id = src[pos : pos + 2]
|
|
pos += 2
|
|
if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
|
|
if escape_id != "\\\n":
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS)
|
|
try:
|
|
char = src[pos]
|
|
except IndexError:
|
|
return pos, ""
|
|
if char != "\n":
|
|
raise v_tomli_suffixed_err(src, pos, "Unescaped '\\' in a string")
|
|
pos += 1
|
|
pos = v_tomli_skip_chars(src, pos, v_tomli_TOML_WS_AND_NEWLINE)
|
|
return pos, ""
|
|
if escape_id == "\\u":
|
|
return v_tomli_parse_hex_char(src, pos, 4)
|
|
if escape_id == "\\U":
|
|
return v_tomli_parse_hex_char(src, pos, 8)
|
|
try:
|
|
return pos, v_tomli_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
|
except KeyError:
|
|
raise v_tomli_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
|
|
|
def v_tomli_parse_basic_str_escape_multiline(
|
|
src: str, pos: v_tomli_Pos
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
return v_tomli_parse_basic_str_escape(src, pos, multiline=True)
|
|
|
|
def v_tomli_parse_hex_char(
|
|
src: str, pos: v_tomli_Pos, hex_len: int
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
hex_str = src[pos : pos + hex_len]
|
|
if len(hex_str) != hex_len or not v_tomli_HEXDIGIT_CHARS.issuperset(hex_str):
|
|
raise v_tomli_suffixed_err(src, pos, "Invalid hex value")
|
|
pos += hex_len
|
|
hex_int = int(hex_str, 16)
|
|
if not v_tomli_is_unicode_scalar_value(hex_int):
|
|
raise v_tomli_suffixed_err(
|
|
src, pos, "Escaped character is not a Unicode scalar value"
|
|
)
|
|
return pos, chr(hex_int)
|
|
|
|
def v_tomli_parse_literal_str(
|
|
src: str, pos: v_tomli_Pos
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
pos += 1
|
|
start_pos = pos
|
|
pos = v_tomli_skip_until(
|
|
src, pos, "'", error_on=v_tomli_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True
|
|
)
|
|
return pos + 1, src[start_pos:pos]
|
|
|
|
def v_tomli_parse_multiline_str(
|
|
src: str, pos: v_tomli_Pos, *, literal: bool
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
pos += 3
|
|
if src.startswith("\n", pos):
|
|
pos += 1
|
|
if literal:
|
|
delim = "'"
|
|
end_pos = v_tomli_skip_until(
|
|
src,
|
|
pos,
|
|
"'''",
|
|
error_on=v_tomli_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
|
error_on_eof=True,
|
|
)
|
|
result = src[pos:end_pos]
|
|
pos = end_pos + 3
|
|
else:
|
|
delim = '"'
|
|
pos, result = v_tomli_parse_basic_str(src, pos, multiline=True)
|
|
if not src.startswith(delim, pos):
|
|
return pos, result
|
|
pos += 1
|
|
if not src.startswith(delim, pos):
|
|
return pos, result + delim
|
|
pos += 1
|
|
return pos, result + delim * 2
|
|
|
|
def v_tomli_parse_basic_str(
|
|
src: str, pos: v_tomli_Pos, *, multiline: bool
|
|
) -> tuple[v_tomli_Pos, str]:
|
|
if multiline:
|
|
error_on = v_tomli_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
|
parse_escapes = v_tomli_parse_basic_str_escape_multiline
|
|
else:
|
|
error_on = v_tomli_ILLEGAL_BASIC_STR_CHARS
|
|
parse_escapes = v_tomli_parse_basic_str_escape
|
|
result = ""
|
|
start_pos = pos
|
|
while True:
|
|
try:
|
|
char = src[pos]
|
|
except IndexError:
|
|
raise v_tomli_suffixed_err(src, pos, "Unterminated string") from None
|
|
if char == '"':
|
|
if not multiline:
|
|
return pos + 1, result + src[start_pos:pos]
|
|
if src.startswith('"""', pos):
|
|
return pos + 3, result + src[start_pos:pos]
|
|
pos += 1
|
|
continue
|
|
if char == "\\":
|
|
result += src[start_pos:pos]
|
|
pos, parsed_escape = parse_escapes(src, pos)
|
|
result += parsed_escape
|
|
start_pos = pos
|
|
continue
|
|
if char in error_on:
|
|
raise v_tomli_suffixed_err(src, pos, f"Illegal character {char!r}")
|
|
pos += 1
|
|
|
|
def v_tomli_parse_value(
|
|
src: str, pos: v_tomli_Pos, parse_float: v_tomli_ParseFloat
|
|
) -> tuple[v_tomli_Pos, Any]:
|
|
try:
|
|
char: str | None = src[pos]
|
|
except IndexError:
|
|
char = None
|
|
if char == '"':
|
|
if src.startswith('"""', pos):
|
|
return v_tomli_parse_multiline_str(src, pos, literal=False)
|
|
return v_tomli_parse_one_line_basic_str(src, pos)
|
|
if char == "'":
|
|
if src.startswith("'''", pos):
|
|
return v_tomli_parse_multiline_str(src, pos, literal=True)
|
|
return v_tomli_parse_literal_str(src, pos)
|
|
if char == "t":
|
|
if src.startswith("true", pos):
|
|
return pos + 4, True
|
|
if char == "f":
|
|
if src.startswith("false", pos):
|
|
return pos + 5, False
|
|
if char == "[":
|
|
return v_tomli_parse_array(src, pos, parse_float)
|
|
if char == "{":
|
|
return v_tomli_parse_inline_table(src, pos, parse_float)
|
|
datetime_match = v_tomli_RE_DATETIME.match(src, pos)
|
|
if datetime_match:
|
|
try:
|
|
datetime_obj = v_tomli_match_to_datetime(datetime_match)
|
|
except ValueError as e:
|
|
raise v_tomli_suffixed_err(src, pos, "Invalid date or datetime") from e
|
|
return datetime_match.end(), datetime_obj
|
|
localtime_match = v_tomli_RE_LOCALTIME.match(src, pos)
|
|
if localtime_match:
|
|
return localtime_match.end(), v_tomli_match_to_localtime(localtime_match)
|
|
number_match = v_tomli_RE_NUMBER.match(src, pos)
|
|
if number_match:
|
|
return number_match.end(), v_tomli_match_to_number(
|
|
number_match, parse_float
|
|
)
|
|
first_three = src[pos : pos + 3]
|
|
if first_three in {"inf", "nan"}:
|
|
return pos + 3, parse_float(first_three)
|
|
first_four = src[pos : pos + 4]
|
|
if first_four in {"-inf", "+inf", "-nan", "+nan"}:
|
|
return pos + 4, parse_float(first_four)
|
|
raise v_tomli_suffixed_err(src, pos, "Invalid value")
|
|
|
|
def v_tomli_suffixed_err(
|
|
src: str, pos: v_tomli_Pos, msg: str
|
|
) -> v_tomli_TOMLDecodeError:
|
|
def coord_repr(src: str, pos: v_tomli_Pos) -> str:
|
|
if pos >= len(src):
|
|
return "end of document"
|
|
line = src.count("\n", 0, pos) + 1
|
|
if line == 1:
|
|
column = pos + 1
|
|
else:
|
|
column = pos - src.rindex("\n", 0, pos)
|
|
return f"line {line}, column {column}"
|
|
|
|
return v_tomli_TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})")
|
|
|
|
def v_tomli_is_unicode_scalar_value(codepoint: int) -> bool:
|
|
return 0 <= codepoint <= 55295 or 57344 <= codepoint <= 1114111
|
|
|
|
def v_tomli_make_safe_parse_float(
|
|
parse_float: v_tomli_ParseFloat,
|
|
) -> v_tomli_ParseFloat:
|
|
if parse_float is float:
|
|
return float
|
|
|
|
def safe_parse_float(float_str: str) -> Any:
|
|
float_value = parse_float(float_str)
|
|
if isinstance(float_value, (dict, list)):
|
|
raise ValueError("parse_float must not return dicts or lists")
|
|
return float_value
|
|
|
|
return safe_parse_float
|
|
|
|
toml_loads = v_tomli_loads
|
|
|
|
#### END VENDORED TOMLI ####
|
|
|
|
|
|
#### START VENDORED PACKAGING ####
|
|
|
|
# MODIFIED FROM https://github.com/pypa/packaging
|
|
# see repo for original licenses
|
|
# This software is made available under the terms of *either* of the licenses
|
|
# found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
|
|
# under the terms of *both* these licenses.
|
|
|
|
import abc # noqa
|
|
import itertools # noqa
|
|
import re # noqa
|
|
from typing import ( # noqa
|
|
Any,
|
|
Callable,
|
|
Iterable,
|
|
Iterator,
|
|
List,
|
|
NamedTuple,
|
|
Optional,
|
|
Set,
|
|
SupportsInt,
|
|
Tuple,
|
|
TypeVar,
|
|
Union,
|
|
)
|
|
|
|
|
|
class v_packaging_InfinityType:
|
|
def __repr__(self) -> str:
|
|
return "v_packaging_Infinity"
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(repr(self))
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
return False
|
|
|
|
def __le__(self, other: object) -> bool:
|
|
return False
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
return isinstance(other, self.__class__)
|
|
|
|
def __gt__(self, other: object) -> bool:
|
|
return True
|
|
|
|
def __ge__(self, other: object) -> bool:
|
|
return True
|
|
|
|
def __neg__(self: object) -> "v_packaging_NegativeInfinityType":
|
|
return v_packaging_NegativeInfinity
|
|
|
|
|
|
v_packaging_Infinity = v_packaging_InfinityType()
|
|
|
|
|
|
class v_packaging_NegativeInfinityType:
|
|
def __repr__(self) -> str:
|
|
return "-Infinity"
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(repr(self))
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
return True
|
|
|
|
def __le__(self, other: object) -> bool:
|
|
return True
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
return isinstance(other, self.__class__)
|
|
|
|
def __gt__(self, other: object) -> bool:
|
|
return False
|
|
|
|
def __ge__(self, other: object) -> bool:
|
|
return False
|
|
|
|
def __neg__(self: object) -> v_packaging_InfinityType:
|
|
return v_packaging_Infinity
|
|
|
|
|
|
v_packaging_NegativeInfinity = v_packaging_NegativeInfinityType()
|
|
v_packaging_LocalType = Tuple[Union[int, str], ...]
|
|
v_packaging_CmpPrePostDevType = Union[
|
|
v_packaging_InfinityType, v_packaging_NegativeInfinityType, Tuple[str, int]
|
|
]
|
|
v_packaging_CmpLocalType = Union[
|
|
v_packaging_NegativeInfinityType,
|
|
Tuple[
|
|
Union[
|
|
Tuple[int, str], Tuple[v_packaging_NegativeInfinityType, Union[int, str]]
|
|
],
|
|
...,
|
|
],
|
|
]
|
|
v_packaging_CmpKey = Tuple[
|
|
int,
|
|
Tuple[int, ...],
|
|
v_packaging_CmpPrePostDevType,
|
|
v_packaging_CmpPrePostDevType,
|
|
v_packaging_CmpPrePostDevType,
|
|
v_packaging_CmpLocalType,
|
|
]
|
|
v_packaging_VersionComparisonMethod = Callable[
|
|
[v_packaging_CmpKey, v_packaging_CmpKey], bool
|
|
]
|
|
|
|
|
|
class v_packaging__Version(NamedTuple):
|
|
epoch: int
|
|
release: Tuple[int, ...]
|
|
dev: Optional[Tuple[str, int]]
|
|
pre: Optional[Tuple[str, int]]
|
|
post: Optional[Tuple[str, int]]
|
|
local: Optional[v_packaging_LocalType]
|
|
|
|
|
|
def v_packaging_parse(version: str) -> "v_packaging_Version":
|
|
return v_packaging_Version(version)
|
|
|
|
|
|
class v_packaging_InvalidVersion(ValueError):
|
|
pass
|
|
|
|
|
|
class v_packaging__BaseVersion:
|
|
_key: Tuple[Any, ...]
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self._key)
|
|
|
|
def __lt__(self, other: "v_packaging__BaseVersion") -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key < other._key
|
|
|
|
def __le__(self, other: "v_packaging__BaseVersion") -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key <= other._key
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key == other._key
|
|
|
|
def __ge__(self, other: "v_packaging__BaseVersion") -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key >= other._key
|
|
|
|
def __gt__(self, other: "v_packaging__BaseVersion") -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key > other._key
|
|
|
|
def __ne__(self, other: object) -> bool:
|
|
if not isinstance(other, v_packaging__BaseVersion):
|
|
return NotImplemented
|
|
return self._key != other._key
|
|
|
|
|
|
v_packaging__VERSION_PATTERN = """
|
|
v?
|
|
(?:
|
|
(?:(?P<epoch>[0-9]+)!)? # epoch
|
|
(?P<release>[0-9]+(?:\\.[0-9]+)*) # release segment
|
|
(?P<pre> # pre-release
|
|
[-_\\.]?
|
|
(?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
|
|
[-_\\.]?
|
|
(?P<pre_n>[0-9]+)?
|
|
)?
|
|
(?P<post> # post release
|
|
(?:-(?P<post_n1>[0-9]+))
|
|
|
|
|
(?:
|
|
[-_\\.]?
|
|
(?P<post_l>post|rev|r)
|
|
[-_\\.]?
|
|
(?P<post_n2>[0-9]+)?
|
|
)
|
|
)?
|
|
(?P<dev> # dev release
|
|
[-_\\.]?
|
|
(?P<dev_l>dev)
|
|
[-_\\.]?
|
|
(?P<dev_n>[0-9]+)?
|
|
)?
|
|
)
|
|
(?:\\+(?P<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))? # local version
|
|
"""
|
|
v_packaging_VERSION_PATTERN = v_packaging__VERSION_PATTERN
|
|
|
|
|
|
class v_packaging_Version(v_packaging__BaseVersion):
|
|
_regex = re.compile(
|
|
"^\\s*" + v_packaging_VERSION_PATTERN + "\\s*$", re.VERBOSE | re.IGNORECASE
|
|
)
|
|
_key: v_packaging_CmpKey
|
|
|
|
def __init__(self, version: str) -> None:
|
|
match = self._regex.search(version)
|
|
if not match:
|
|
raise v_packaging_InvalidVersion(f"Invalid version: '{version}'")
|
|
self._version = v_packaging__Version(
|
|
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
|
|
release=tuple(int(i) for i in match.group("release").split(".")),
|
|
pre=v_packaging__parse_letter_version(
|
|
match.group("pre_l"), match.group("pre_n")
|
|
),
|
|
post=v_packaging__parse_letter_version(
|
|
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
|
|
),
|
|
dev=v_packaging__parse_letter_version(
|
|
match.group("dev_l"), match.group("dev_n")
|
|
),
|
|
local=v_packaging__parse_local_version(match.group("local")),
|
|
)
|
|
self._key = v_packaging__cmpkey(
|
|
self._version.epoch,
|
|
self._version.release,
|
|
self._version.pre,
|
|
self._version.post,
|
|
self._version.dev,
|
|
self._version.local,
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Version('{self}')>"
|
|
|
|
def __str__(self) -> str:
|
|
parts = []
|
|
if self.epoch != 0:
|
|
parts.append(f"{self.epoch}!")
|
|
parts.append(".".join(str(x) for x in self.release))
|
|
if self.pre is not None:
|
|
parts.append("".join(str(x) for x in self.pre))
|
|
if self.post is not None:
|
|
parts.append(f".post{self.post}")
|
|
if self.dev is not None:
|
|
parts.append(f".dev{self.dev}")
|
|
if self.local is not None:
|
|
parts.append(f"+{self.local}")
|
|
return "".join(parts)
|
|
|
|
@property
|
|
def epoch(self) -> int:
|
|
return self._version.epoch
|
|
|
|
@property
|
|
def release(self) -> Tuple[int, ...]:
|
|
return self._version.release
|
|
|
|
@property
|
|
def pre(self) -> Optional[Tuple[str, int]]:
|
|
return self._version.pre
|
|
|
|
@property
|
|
def post(self) -> Optional[int]:
|
|
return self._version.post[1] if self._version.post else None
|
|
|
|
@property
|
|
def dev(self) -> Optional[int]:
|
|
return self._version.dev[1] if self._version.dev else None
|
|
|
|
@property
|
|
def local(self) -> Optional[str]:
|
|
if self._version.local:
|
|
return ".".join(str(x) for x in self._version.local)
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def public(self) -> str:
|
|
return str(self).split("+", 1)[0]
|
|
|
|
@property
|
|
def base_version(self) -> str:
|
|
parts = []
|
|
if self.epoch != 0:
|
|
parts.append(f"{self.epoch}!")
|
|
parts.append(".".join(str(x) for x in self.release))
|
|
return "".join(parts)
|
|
|
|
@property
|
|
def is_prerelease(self) -> bool:
|
|
return self.dev is not None or self.pre is not None
|
|
|
|
@property
|
|
def is_postrelease(self) -> bool:
|
|
return self.post is not None
|
|
|
|
@property
|
|
def is_devrelease(self) -> bool:
|
|
return self.dev is not None
|
|
|
|
@property
|
|
def major(self) -> int:
|
|
return self.release[0] if len(self.release) >= 1 else 0
|
|
|
|
@property
|
|
def minor(self) -> int:
|
|
return self.release[1] if len(self.release) >= 2 else 0
|
|
|
|
@property
|
|
def micro(self) -> int:
|
|
return self.release[2] if len(self.release) >= 3 else 0
|
|
|
|
|
|
def v_packaging__parse_letter_version(
|
|
letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
|
|
) -> Optional[Tuple[str, int]]:
|
|
if letter:
|
|
if number is None:
|
|
number = 0
|
|
letter = letter.lower()
|
|
if letter == "alpha":
|
|
letter = "a"
|
|
elif letter == "beta":
|
|
letter = "b"
|
|
elif letter in ["c", "pre", "preview"]:
|
|
letter = "rc"
|
|
elif letter in ["rev", "r"]:
|
|
letter = "post"
|
|
return letter, int(number)
|
|
if not letter and number:
|
|
letter = "post"
|
|
return letter, int(number)
|
|
return None
|
|
|
|
|
|
v_packaging__local_version_separators = re.compile("[\\._-]")
|
|
|
|
|
|
def v_packaging__parse_local_version(
|
|
local: Optional[str],
|
|
) -> Optional[v_packaging_LocalType]:
|
|
if local is not None:
|
|
return tuple(
|
|
part.lower() if not part.isdigit() else int(part)
|
|
for part in v_packaging__local_version_separators.split(local)
|
|
)
|
|
return None
|
|
|
|
|
|
def v_packaging__cmpkey(
|
|
epoch: int,
|
|
release: Tuple[int, ...],
|
|
pre: Optional[Tuple[str, int]],
|
|
post: Optional[Tuple[str, int]],
|
|
dev: Optional[Tuple[str, int]],
|
|
local: Optional[v_packaging_LocalType],
|
|
) -> v_packaging_CmpKey:
|
|
_release = tuple(
|
|
reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
|
|
)
|
|
if pre is None and post is None and dev is not None:
|
|
_pre: v_packaging_CmpPrePostDevType = v_packaging_NegativeInfinity
|
|
elif pre is None:
|
|
_pre = v_packaging_Infinity
|
|
else:
|
|
_pre = pre
|
|
if post is None:
|
|
_post: v_packaging_CmpPrePostDevType = v_packaging_NegativeInfinity
|
|
else:
|
|
_post = post
|
|
if dev is None:
|
|
_dev: v_packaging_CmpPrePostDevType = v_packaging_Infinity
|
|
else:
|
|
_dev = dev
|
|
if local is None:
|
|
_local: v_packaging_CmpLocalType = v_packaging_NegativeInfinity
|
|
else:
|
|
_local = tuple(
|
|
(i, "") if isinstance(i, int) else (v_packaging_NegativeInfinity, i)
|
|
for i in local
|
|
)
|
|
return epoch, _release, _pre, _post, _dev, _local
|
|
|
|
|
|
def v_packaging_canonicalize_version(
|
|
version: Union[v_packaging_Version, str], *, strip_trailing_zero: bool = True
|
|
) -> str:
|
|
if isinstance(version, str):
|
|
try:
|
|
v_packaging_parsed = v_packaging_Version(version)
|
|
except v_packaging_InvalidVersion:
|
|
return version
|
|
else:
|
|
v_packaging_parsed = version
|
|
parts = []
|
|
if v_packaging_parsed.epoch != 0:
|
|
parts.append(f"{v_packaging_parsed.epoch}!")
|
|
release_segment = ".".join(str(x) for x in v_packaging_parsed.release)
|
|
if strip_trailing_zero:
|
|
release_segment = re.sub("(\\.0)+$", "", release_segment)
|
|
parts.append(release_segment)
|
|
if v_packaging_parsed.pre is not None:
|
|
parts.append("".join(str(x) for x in v_packaging_parsed.pre))
|
|
if v_packaging_parsed.post is not None:
|
|
parts.append(f".post{v_packaging_parsed.post}")
|
|
if v_packaging_parsed.dev is not None:
|
|
parts.append(f".dev{v_packaging_parsed.dev}")
|
|
if v_packaging_parsed.local is not None:
|
|
parts.append(f"+{v_packaging_parsed.local}")
|
|
return "".join(parts)
|
|
|
|
|
|
v_packaging_UnparsedVersion = Union[v_packaging_Version, str]
|
|
v_packaging_UnparsedVersionVar = TypeVar(
|
|
"v_packaging_UnparsedVersionVar", bound=v_packaging_UnparsedVersion
|
|
)
|
|
v_packaging_CallableOperator = Callable[[v_packaging_Version, str], bool]
|
|
|
|
|
|
def v_packaging__coerce_version(
|
|
version: v_packaging_UnparsedVersion,
|
|
) -> v_packaging_Version:
|
|
if not isinstance(version, v_packaging_Version):
|
|
version = v_packaging_Version(version)
|
|
return version
|
|
|
|
|
|
class v_packaging_InvalidSpecifier(ValueError):
|
|
pass
|
|
|
|
|
|
class v_packaging_BaseSpecifier(metaclass=abc.ABCMeta):
|
|
@abc.abstractmethod
|
|
def __str__(self) -> str:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def __hash__(self) -> int:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def __eq__(self, other: object) -> bool:
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def prereleases(self) -> Optional[bool]:
|
|
pass
|
|
|
|
@prereleases.setter
|
|
def prereleases(self, value: bool) -> None:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def filter(
|
|
self,
|
|
iterable: Iterable[v_packaging_UnparsedVersionVar],
|
|
prereleases: Optional[bool] = None,
|
|
) -> Iterator[v_packaging_UnparsedVersionVar]:
|
|
pass
|
|
|
|
|
|
class v_packaging_Specifier(v_packaging_BaseSpecifier):
|
|
_operator_regex_str = """
|
|
(?P<operator>(~=|==|!=|<=|>=|<|>|===))
|
|
"""
|
|
_version_regex_str = """
|
|
(?P<version>
|
|
(?:
|
|
# The identity operators allow for an escape hatch that will
|
|
# do an exact string match of the version you wish to install.
|
|
# This will not be v_packaging_parsed by PEP 440 and we cannot determine
|
|
# any semantic meaning from it. This operator is discouraged
|
|
# but included entirely as an escape hatch.
|
|
(?<====) # Only match for the identity operator
|
|
\\s*
|
|
[^\\s;)]* # The arbitrary version can be just about anything,
|
|
# we match everything except for whitespace, a
|
|
# semi-colon for marker support, and a closing paren
|
|
# since versions can be enclosed in them.
|
|
)
|
|
|
|
|
(?:
|
|
# The (non)equality operators allow for wild card and local
|
|
# versions to be specified so we have to define these two
|
|
# operators separately to enable that.
|
|
(?<===|!=) # Only match for equals and not equals
|
|
\\s*
|
|
v?
|
|
(?:[0-9]+!)? # epoch
|
|
[0-9]+(?:\\.[0-9]+)* # release
|
|
# You cannot use a wild card and a pre-release, post-release, a dev or
|
|
# local version together so group them with a | and make them optional.
|
|
(?:
|
|
\\.\\* # Wild card syntax of .*
|
|
|
|
|
(?: # pre release
|
|
[-_\\.]?
|
|
(alpha|beta|preview|pre|a|b|c|rc)
|
|
[-_\\.]?
|
|
[0-9]*
|
|
)?
|
|
(?: # post release
|
|
(?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)
|
|
)?
|
|
(?:[-_\\.]?dev[-_\\.]?[0-9]*)? # dev release
|
|
(?:\\+[a-z0-9]+(?:[-_\\.][a-z0-9]+)*)? # local
|
|
)?
|
|
)
|
|
|
|
|
(?:
|
|
# The compatible operator requires at least two digits in the
|
|
# release segment.
|
|
(?<=~=) # Only match for the compatible operator
|
|
\\s*
|
|
v?
|
|
(?:[0-9]+!)? # epoch
|
|
[0-9]+(?:\\.[0-9]+)+ # release (We have a + instead of a *)
|
|
(?: # pre release
|
|
[-_\\.]?
|
|
(alpha|beta|preview|pre|a|b|c|rc)
|
|
[-_\\.]?
|
|
[0-9]*
|
|
)?
|
|
(?: # post release
|
|
(?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)
|
|
)?
|
|
(?:[-_\\.]?dev[-_\\.]?[0-9]*)? # dev release
|
|
)
|
|
|
|
|
(?:
|
|
# All other operators only allow a sub set of what the
|
|
# (non)equality operators do. Specifically they do not allow
|
|
# local versions to be specified nor do they allow the prefix
|
|
# matching wild cards.
|
|
(?<!==|!=|~=) # We have special cases for these
|
|
# operators so we want to make sure they
|
|
# don't match here.
|
|
\\s*
|
|
v?
|
|
(?:[0-9]+!)? # epoch
|
|
[0-9]+(?:\\.[0-9]+)* # release
|
|
(?: # pre release
|
|
[-_\\.]?
|
|
(alpha|beta|preview|pre|a|b|c|rc)
|
|
[-_\\.]?
|
|
[0-9]*
|
|
)?
|
|
(?: # post release
|
|
(?:-[0-9]+)|(?:[-_\\.]?(post|rev|r)[-_\\.]?[0-9]*)
|
|
)?
|
|
(?:[-_\\.]?dev[-_\\.]?[0-9]*)? # dev release
|
|
)
|
|
)
|
|
"""
|
|
_regex = re.compile(
|
|
"^\\s*" + _operator_regex_str + _version_regex_str + "\\s*$",
|
|
re.VERBOSE | re.IGNORECASE,
|
|
)
|
|
_operators = {
|
|
"~=": "compatible",
|
|
"==": "equal",
|
|
"!=": "not_equal",
|
|
"<=": "less_than_equal",
|
|
">=": "greater_than_equal",
|
|
"<": "less_than",
|
|
">": "greater_than",
|
|
"===": "arbitrary",
|
|
}
|
|
|
|
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
|
|
match = self._regex.search(spec)
|
|
if not match:
|
|
raise v_packaging_InvalidSpecifier(f"Invalid specifier: '{spec}'")
|
|
self._spec: Tuple[str, str] = (
|
|
match.group("operator").strip(),
|
|
match.group("version").strip(),
|
|
)
|
|
self._prereleases = prereleases
|
|
|
|
@property
|
|
def prereleases(self) -> bool:
|
|
if self._prereleases is not None:
|
|
return self._prereleases
|
|
operator, version = self._spec
|
|
if operator in ["==", ">=", "<=", "~=", "==="]:
|
|
if operator == "==" and version.endswith(".*"):
|
|
version = version[:-2]
|
|
if v_packaging_Version(version).is_prerelease:
|
|
return True
|
|
return False
|
|
|
|
@prereleases.setter
|
|
def prereleases(self, value: bool) -> None:
|
|
self._prereleases = value
|
|
|
|
@property
|
|
def operator(self) -> str:
|
|
return self._spec[0]
|
|
|
|
@property
|
|
def version(self) -> str:
|
|
return self._spec[1]
|
|
|
|
def __repr__(self) -> str:
|
|
pre = (
|
|
f", prereleases={self.prereleases!r}"
|
|
if self._prereleases is not None
|
|
else ""
|
|
)
|
|
return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
|
|
|
|
def __str__(self) -> str:
|
|
return "{}{}".format(*self._spec)
|
|
|
|
@property
|
|
def _canonical_spec(self) -> Tuple[str, str]:
|
|
canonical_version = v_packaging_canonicalize_version(
|
|
self._spec[1], strip_trailing_zero=self._spec[0] != "~="
|
|
)
|
|
return self._spec[0], canonical_version
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self._canonical_spec)
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, str):
|
|
try:
|
|
other = self.__class__(str(other))
|
|
except v_packaging_InvalidSpecifier:
|
|
return NotImplemented
|
|
elif not isinstance(other, self.__class__):
|
|
return NotImplemented
|
|
return self._canonical_spec == other._canonical_spec
|
|
|
|
def _get_operator(self, op: str) -> v_packaging_CallableOperator:
|
|
operator_callable: v_packaging_CallableOperator = getattr(
|
|
self, f"_compare_{self._operators[op]}"
|
|
)
|
|
return operator_callable
|
|
|
|
def _compare_compatible(self, prospective: v_packaging_Version, spec: str) -> bool:
|
|
prefix = ".".join(
|
|
list(
|
|
itertools.takewhile(
|
|
v_packaging__is_not_suffix, v_packaging__version_split(spec)
|
|
)
|
|
)[:-1]
|
|
)
|
|
prefix += ".*"
|
|
return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
|
|
prospective, prefix
|
|
)
|
|
|
|
def _compare_equal(self, prospective: v_packaging_Version, spec: str) -> bool:
|
|
if spec.endswith(".*"):
|
|
normalized_prospective = v_packaging_canonicalize_version(
|
|
prospective.public, strip_trailing_zero=False
|
|
)
|
|
normalized_spec = v_packaging_canonicalize_version(
|
|
spec[:-2], strip_trailing_zero=False
|
|
)
|
|
split_spec = v_packaging__version_split(normalized_spec)
|
|
split_prospective = v_packaging__version_split(normalized_prospective)
|
|
padded_prospective, _ = v_packaging__pad_version(
|
|
split_prospective, split_spec
|
|
)
|
|
shortened_prospective = padded_prospective[: len(split_spec)]
|
|
return shortened_prospective == split_spec
|
|
else:
|
|
spec_version = v_packaging_Version(spec)
|
|
if not spec_version.local:
|
|
prospective = v_packaging_Version(prospective.public)
|
|
return prospective == spec_version
|
|
|
|
def _compare_not_equal(self, prospective: v_packaging_Version, spec: str) -> bool:
|
|
return not self._compare_equal(prospective, spec)
|
|
|
|
def _compare_less_than_equal(
|
|
self, prospective: v_packaging_Version, spec: str
|
|
) -> bool:
|
|
return v_packaging_Version(prospective.public) <= v_packaging_Version(spec)
|
|
|
|
def _compare_greater_than_equal(
|
|
self, prospective: v_packaging_Version, spec: str
|
|
) -> bool:
|
|
return v_packaging_Version(prospective.public) >= v_packaging_Version(spec)
|
|
|
|
def _compare_less_than(
|
|
self, prospective: v_packaging_Version, spec_str: str
|
|
) -> bool:
|
|
spec = v_packaging_Version(spec_str)
|
|
if not prospective < spec:
|
|
return False
|
|
if not spec.is_prerelease and prospective.is_prerelease:
|
|
if v_packaging_Version(prospective.base_version) == v_packaging_Version(
|
|
spec.base_version
|
|
):
|
|
return False
|
|
return True
|
|
|
|
def _compare_greater_than(
|
|
self, prospective: v_packaging_Version, spec_str: str
|
|
) -> bool:
|
|
spec = v_packaging_Version(spec_str)
|
|
if not prospective > spec:
|
|
return False
|
|
if not spec.is_postrelease and prospective.is_postrelease:
|
|
if v_packaging_Version(prospective.base_version) == v_packaging_Version(
|
|
spec.base_version
|
|
):
|
|
return False
|
|
if prospective.local is not None:
|
|
if v_packaging_Version(prospective.base_version) == v_packaging_Version(
|
|
spec.base_version
|
|
):
|
|
return False
|
|
return True
|
|
|
|
def _compare_arbitrary(self, prospective: v_packaging_Version, spec: str) -> bool:
|
|
return str(prospective).lower() == str(spec).lower()
|
|
|
|
def __contains__(self, item: Union[str, v_packaging_Version]) -> bool:
|
|
return self.contains(item)
|
|
|
|
def contains(
|
|
self, item: v_packaging_UnparsedVersion, prereleases: Optional[bool] = None
|
|
) -> bool:
|
|
if prereleases is None:
|
|
prereleases = self.prereleases
|
|
normalized_item = v_packaging__coerce_version(item)
|
|
if normalized_item.is_prerelease and not prereleases:
|
|
return False
|
|
operator_callable: v_packaging_CallableOperator = self._get_operator(
|
|
self.operator
|
|
)
|
|
return operator_callable(normalized_item, self.version)
|
|
|
|
def filter(
|
|
self,
|
|
iterable: Iterable[v_packaging_UnparsedVersionVar],
|
|
prereleases: Optional[bool] = None,
|
|
) -> Iterator[v_packaging_UnparsedVersionVar]:
|
|
yielded = False
|
|
found_prereleases = []
|
|
kw = {"prereleases": prereleases if prereleases is not None else True}
|
|
for version in iterable:
|
|
v_packaging_parsed_version = v_packaging__coerce_version(version)
|
|
if self.contains(v_packaging_parsed_version, **kw):
|
|
if v_packaging_parsed_version.is_prerelease and not (
|
|
prereleases or self.prereleases
|
|
):
|
|
found_prereleases.append(version)
|
|
else:
|
|
yielded = True
|
|
yield version
|
|
if not yielded and found_prereleases:
|
|
for version in found_prereleases:
|
|
yield version
|
|
|
|
|
|
v_packaging__prefix_regex = re.compile("^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
|
|
|
|
|
|
def v_packaging__version_split(version: str) -> List[str]:
|
|
result: List[str] = []
|
|
for item in version.split("."):
|
|
match = v_packaging__prefix_regex.search(item)
|
|
if match:
|
|
result.extend(match.groups())
|
|
else:
|
|
result.append(item)
|
|
return result
|
|
|
|
|
|
def v_packaging__is_not_suffix(segment: str) -> bool:
|
|
return not any(
|
|
segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
|
|
)
|
|
|
|
|
|
def v_packaging__pad_version(
|
|
left: List[str], right: List[str]
|
|
) -> Tuple[List[str], List[str]]:
|
|
left_split, right_split = [], []
|
|
left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
|
|
right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
|
|
left_split.append(left[len(left_split[0]) :])
|
|
right_split.append(right[len(right_split[0]) :])
|
|
left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
|
|
right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
|
|
return list(itertools.chain(*left_split)), list(itertools.chain(*right_split))
|
|
|
|
|
|
class v_packaging_SpecifierSet(v_packaging_BaseSpecifier):
|
|
def __init__(
|
|
self, specifiers: str = "", prereleases: Optional[bool] = None
|
|
) -> None:
|
|
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
|
|
v_packaging_parsed: Set[v_packaging_Specifier] = set()
|
|
for specifier in split_specifiers:
|
|
v_packaging_parsed.add(v_packaging_Specifier(specifier))
|
|
self._specs = frozenset(v_packaging_parsed)
|
|
self._prereleases = prereleases
|
|
|
|
@property
|
|
def prereleases(self) -> Optional[bool]:
|
|
if self._prereleases is not None:
|
|
return self._prereleases
|
|
if not self._specs:
|
|
return None
|
|
return any(s.prereleases for s in self._specs)
|
|
|
|
@prereleases.setter
|
|
def prereleases(self, value: bool) -> None:
|
|
self._prereleases = value
|
|
|
|
def __repr__(self) -> str:
|
|
pre = (
|
|
f", prereleases={self.prereleases!r}"
|
|
if self._prereleases is not None
|
|
else ""
|
|
)
|
|
return f"<SpecifierSet({str(self)!r}{pre})>"
|
|
|
|
def __str__(self) -> str:
|
|
return ",".join(sorted(str(s) for s in self._specs))
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self._specs)
|
|
|
|
def __and__(
|
|
self, other: Union["v_packaging_SpecifierSet", str]
|
|
) -> "v_packaging_SpecifierSet":
|
|
if isinstance(other, str):
|
|
other = v_packaging_SpecifierSet(other)
|
|
elif not isinstance(other, v_packaging_SpecifierSet):
|
|
return NotImplemented
|
|
specifier = v_packaging_SpecifierSet()
|
|
specifier._specs = frozenset(self._specs | other._specs)
|
|
if self._prereleases is None and other._prereleases is not None:
|
|
specifier._prereleases = other._prereleases
|
|
elif self._prereleases is not None and other._prereleases is None:
|
|
specifier._prereleases = self._prereleases
|
|
elif self._prereleases == other._prereleases:
|
|
specifier._prereleases = self._prereleases
|
|
else:
|
|
raise ValueError(
|
|
"Cannot combine v_packaging_SpecifierSets "
|
|
"with True and False prerelease overrides."
|
|
)
|
|
return specifier
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, (str, v_packaging_Specifier)):
|
|
other = v_packaging_SpecifierSet(str(other))
|
|
elif not isinstance(other, v_packaging_SpecifierSet):
|
|
return NotImplemented
|
|
return self._specs == other._specs
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._specs)
|
|
|
|
def __iter__(self) -> Iterator[v_packaging_Specifier]:
|
|
return iter(self._specs)
|
|
|
|
def __contains__(self, item: v_packaging_UnparsedVersion) -> bool:
|
|
return self.contains(item)
|
|
|
|
def contains(
|
|
self,
|
|
item: v_packaging_UnparsedVersion,
|
|
prereleases: Optional[bool] = None,
|
|
installed: Optional[bool] = None,
|
|
) -> bool:
|
|
if not isinstance(item, v_packaging_Version):
|
|
item = v_packaging_Version(item)
|
|
if prereleases is None:
|
|
prereleases = self.prereleases
|
|
if not prereleases and item.is_prerelease:
|
|
return False
|
|
if installed and item.is_prerelease:
|
|
item = v_packaging_Version(item.base_version)
|
|
return all(s.contains(item, prereleases=prereleases) for s in self._specs)
|
|
|
|
def filter(
|
|
self,
|
|
iterable: Iterable[v_packaging_UnparsedVersionVar],
|
|
prereleases: Optional[bool] = None,
|
|
) -> Iterator[v_packaging_UnparsedVersionVar]:
|
|
if prereleases is None:
|
|
prereleases = self.prereleases
|
|
if self._specs:
|
|
for spec in self._specs:
|
|
iterable = spec.filter(iterable, prereleases=bool(prereleases))
|
|
return iter(iterable)
|
|
else:
|
|
filtered: List[v_packaging_UnparsedVersionVar] = []
|
|
found_prereleases: List[v_packaging_UnparsedVersionVar] = []
|
|
for item in iterable:
|
|
v_packaging_parsed_version = v_packaging__coerce_version(item)
|
|
if v_packaging_parsed_version.is_prerelease and not prereleases:
|
|
if not filtered:
|
|
found_prereleases.append(item)
|
|
else:
|
|
filtered.append(item)
|
|
if not filtered and found_prereleases and prereleases is None:
|
|
return iter(found_prereleases)
|
|
return iter(filtered)
|
|
|
|
|
|
Version = v_packaging_Version
|
|
SpecifierSet = v_packaging_SpecifierSet
|
|
|
|
#### END VENDORED PACKAGING ####
|
|
|
|
|
|
class Spinner:
|
|
"""spinner modified from:
|
|
https://raw.githubusercontent.com/Tagar/stuff/master/spinner.py
|
|
"""
|
|
|
|
def __init__(self, message: str, delay: float = 0.1) -> None:
|
|
self.spinner = itertools.cycle([f"{c} " for c in "⣾⣽⣻⢿⡿⣟⣯⣷"])
|
|
self.delay = delay
|
|
self.busy = False
|
|
self.spinner_visible = False
|
|
self.message = message
|
|
sys.stderr.write(f"{a.prefix} {a.sep} {message} ")
|
|
|
|
def write_next(self) -> None:
|
|
with self._screen_lock:
|
|
if not self.spinner_visible:
|
|
sys.stderr.write(next(self.spinner))
|
|
self.spinner_visible = True
|
|
sys.stderr.flush()
|
|
|
|
def remove_spinner(self, cleanup: bool = False) -> None:
|
|
with self._screen_lock:
|
|
if self.spinner_visible:
|
|
sys.stderr.write("\b\b\b")
|
|
self.spinner_visible = False
|
|
if cleanup:
|
|
sys.stderr.write(" ") # overwrite spinner with blank
|
|
sys.stderr.write("\r\033[K")
|
|
sys.stderr.flush()
|
|
|
|
def spinner_task(self) -> None:
|
|
while self.busy:
|
|
self.write_next()
|
|
sleep(self.delay)
|
|
self.remove_spinner()
|
|
|
|
def __enter__(self) -> None:
|
|
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: Optional[Type[BaseException]],
|
|
exc_val: Optional[BaseException],
|
|
exc_traceback: Optional[TracebackType],
|
|
) -> None:
|
|
if sys.stderr.isatty():
|
|
self.busy = False
|
|
self.remove_spinner(cleanup=True)
|
|
else:
|
|
sys.stderr.write("\r")
|
|
|
|
|
|
def _path_ok(p: Path) -> Path:
|
|
p.mkdir(exist_ok=True, parents=True)
|
|
return p
|
|
|
|
|
|
class Env:
|
|
defaults = dict(
|
|
viv_bin_dir=Path.home() / ".local" / "bin",
|
|
xdg_cache_home=Path.home() / ".cache",
|
|
xdg_data_home=Path.home() / ".local" / "share",
|
|
)
|
|
|
|
def __getattr__(self, attr: str) -> Any:
|
|
if (
|
|
not attr.startswith("_")
|
|
and (defined := getattr(self, f"_{attr}")) is not None
|
|
):
|
|
return defined
|
|
else:
|
|
return os.getenv(attr.upper(), self.defaults.get(attr))
|
|
|
|
@property
|
|
def _viv_cache(self) -> Path:
|
|
return Path(os.getenv("VIV_CACHE", (Path(self.xdg_cache_home) / "viv")))
|
|
|
|
@property
|
|
def _viv_spec(self) -> List[str]:
|
|
return [i[1:-1] for i in os.getenv("VIV_SPEC", "").split(" ") if i]
|
|
|
|
@property
|
|
def _viv_log_path(self) -> Path:
|
|
return _path_ok(Path(self.xdg_data_home) / "viv") / "viv.log"
|
|
|
|
@property
|
|
def _viv_run_mode(self) -> str:
|
|
choices = {"ephemeral", "semi-ephemeral", "persist"}
|
|
run_mode = os.getenv("VIV_RUN_MODE", "ephemeral")
|
|
if run_mode not in choices:
|
|
err_quit(
|
|
f"unsupported VIV_RUN_MODE: {run_mode} \noptions: "
|
|
+ ", ".join(
|
|
(f"{a.bold}{a.yellow}{choice}{a.end}" for choice in choices)
|
|
)
|
|
)
|
|
return run_mode
|
|
|
|
|
|
class System:
|
|
def __init__(self) -> None:
|
|
self._windows = platform.system() == "Windows"
|
|
(self.bin_dir, *_) = ("Scripts",) if self._windows else ("bin",)
|
|
|
|
def bin(self, exe: str) -> str:
|
|
return f"{exe}.exe" if self._windows else exe
|
|
|
|
|
|
system = System()
|
|
|
|
|
|
class Cfg:
|
|
def __init__(self) -> None:
|
|
self.windows = platform.system() == "Windows"
|
|
|
|
@property
|
|
def src(self) -> Path:
|
|
p = Path(Env().xdg_data_home) / "viv" / "viv.py"
|
|
p.parent.mkdir(exist_ok=True, parents=True)
|
|
return p
|
|
|
|
@property
|
|
def cache_base(self) -> Path:
|
|
return Env().viv_cache
|
|
|
|
@property
|
|
def cache_src(self) -> Path:
|
|
return _path_ok(self.cache_base / "src")
|
|
|
|
@property
|
|
def cache_venv(self) -> Path:
|
|
return _path_ok(self.cache_base / "venvs")
|
|
|
|
|
|
class Ansi:
|
|
"""control ouptut of ansi(VT100) control codes"""
|
|
|
|
def __init__(self) -> None:
|
|
self.bold = "\033[1m"
|
|
self.dim = "\033[2m"
|
|
self.underline = "\033[4m"
|
|
self.red = "\033[1;31m"
|
|
self.green = "\033[1;32m"
|
|
self.yellow = "\033[1;33m"
|
|
self.magenta = "\033[1;35m"
|
|
self.cyan = "\033[1;36m"
|
|
self.end = "\033[0m"
|
|
|
|
# for argparse help
|
|
self.header = self.cyan
|
|
self.option = self.yellow
|
|
self.metavar = "\033[33m" # normal yellow
|
|
|
|
if not Env().force_color and (Env().no_color or not sys.stderr.isatty()):
|
|
for attr in self.__dict__:
|
|
setattr(self, attr, "")
|
|
|
|
self._ansi_escape = re.compile(
|
|
r"""
|
|
\x1B # ESC
|
|
(?: # 7-bit C1 Fe (except CSI)
|
|
[@-Z\\-_]
|
|
| # or [ for CSI, followed by a control sequence
|
|
\[
|
|
[0-?]* # Parameter bytes
|
|
[ -/]* # Intermediate bytes
|
|
[@-~] # Final byte
|
|
)
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
self.sep = f"{self.magenta}|{self.end}"
|
|
self.prefix = f"{self.cyan}viv{self.end}"
|
|
|
|
def escape(self, txt: str) -> str:
|
|
return self._ansi_escape.sub("", txt)
|
|
|
|
def style(self, txt: str, style: str = "cyan") -> str:
|
|
"""style text with given style
|
|
Args:
|
|
txt: text to stylize
|
|
style: color/style to apply to text
|
|
Returns:
|
|
ansi escape code stylized text
|
|
"""
|
|
return f"{getattr(self,style)}{txt}{getattr(self,'end')}"
|
|
|
|
def tagline(self) -> str:
|
|
"""generate the viv tagline"""
|
|
|
|
return " ".join(
|
|
(
|
|
self.style(f, "magenta") + self.style(rest, "cyan")
|
|
for f, rest in (("v", "iv"), ("i", "sn't"), ("v", "env"))
|
|
)
|
|
)
|
|
|
|
def key_value(self, items: Dict[str, Any], indent: str = " ") -> None:
|
|
for k, v in items.items():
|
|
sys.stderr.write(f"{indent}{a.bold}{k}{a.end}: {v}\n")
|
|
|
|
def subprocess(self, command: List[str], output: str) -> None:
|
|
"""generate output for subprocess error
|
|
|
|
Args:
|
|
output: text output from subprocess, usually from p.stdout
|
|
"""
|
|
if not output:
|
|
return
|
|
|
|
log.error("subprocess failed")
|
|
log.error("see below for command output")
|
|
log.error(f"cmd:\n {' '.join(command)}")
|
|
new_output = [f"{self.red}->{self.end} {line}" for line in output.splitlines()]
|
|
log.error("subprocess output:" + "\n".join(("", *new_output, "")))
|
|
|
|
|
|
a = Ansi()
|
|
|
|
|
|
class MutlilineFormatter(logging.Formatter):
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
outlines = []
|
|
lines = (save_msg := record.msg).splitlines()
|
|
for line in lines:
|
|
record.msg = line
|
|
outlines.append(super().format(record))
|
|
record.msg = save_msg
|
|
record.message = (output := "\n".join(outlines))
|
|
return output
|
|
|
|
|
|
class CustomFormatter(logging.Formatter):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.FORMATS = {
|
|
**{
|
|
level: " ".join(
|
|
(
|
|
a.prefix,
|
|
f"{a.sep}{color}%(levelname)s{a.end}{a.sep}",
|
|
"%(message)s",
|
|
)
|
|
)
|
|
for level, color in {
|
|
logging.DEBUG: a.dim,
|
|
logging.WARNING: a.yellow,
|
|
logging.ERROR: a.red,
|
|
logging.CRITICAL: a.red,
|
|
}.items()
|
|
},
|
|
logging.INFO: f"{a.prefix} {a.sep} %(message)s",
|
|
}
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
log_fmt = self.FORMATS.get(record.levelno)
|
|
formatter = MutlilineFormatter(log_fmt)
|
|
return formatter.format(record)
|
|
|
|
|
|
class CustomFileHandler(RotatingFileHandler):
|
|
"""Custom logging handler to strip ansi before logging to file"""
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
record.msg = a.escape(record.msg)
|
|
super().emit(record)
|
|
|
|
|
|
def gen_logger() -> logging.Logger:
|
|
logger = logging.getLogger("viv")
|
|
if not logger.handlers:
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
ch = logging.StreamHandler()
|
|
ch.setLevel(logging.INFO if not Env().viv_debug else logging.DEBUG)
|
|
ch.setFormatter(CustomFormatter())
|
|
|
|
fh = CustomFileHandler(
|
|
Env().viv_log_path, maxBytes=10 * 1024 * 1024, backupCount=5
|
|
)
|
|
fh.setLevel(logging.DEBUG)
|
|
fh.setFormatter(
|
|
MutlilineFormatter("%(asctime)s | %(levelname)8s | %(message)s")
|
|
)
|
|
|
|
logger.addHandler(ch)
|
|
logger.addHandler(fh)
|
|
return logger
|
|
|
|
|
|
log = gen_logger()
|
|
|
|
|
|
def err_quit(*msg: str, code: int = 1) -> NoReturn:
|
|
log.error("\n".join(msg))
|
|
sys.exit(code)
|
|
|
|
|
|
class Template:
|
|
_standalone_func = r"""def _viv_use(*pkgs, track_exe=False, name=""):
|
|
import hashlib, json, os, site, shutil, sys, venv # noqa
|
|
from pathlib import Path # noqa
|
|
from datetime import datetime # noqa
|
|
from subprocess import run # noqa
|
|
|
|
if not {*map(type, pkgs)} == {str}:
|
|
raise ValueError(f"spec: {pkgs} is invalid")
|
|
|
|
meta = dict.fromkeys(("created", "accessed"), (t := str(datetime.today())))
|
|
runner = str(Path(__file__).absolute().resolve())
|
|
envvar = lambda x: os.getenv(f"VIV_{x}") # noqa
|
|
nopkgs = lambda p: not p.endswith(("dist-packages", "site-packages")) # noqa
|
|
F, V, no_st = map(envvar, ("FORCE", "VERBOSE", "NO_SETUPTOOLS"))
|
|
base = Path(xdg) if (xdg := os.getenv("XDG_CACHE_HOME")) else Path.home() / ".cache"
|
|
(cache := (base) / "viv/venvs").mkdir(parents=True, exist_ok=True)
|
|
exe = str(Path(sys.executable).resolve()) if track_exe else "N/A"
|
|
_id = hashlib.sha256((str(spec := [*pkgs]) + exe).encode()).hexdigest()
|
|
if (env := cache / (name if name else _id[:8])) not in cache.glob("*/") or F:
|
|
sys.stderr.write(f"generating new vivenv -> {env.name}\n")
|
|
venv.create(env, prompt=f"viv-{name}", symlinks=True, clear=True)
|
|
kw = dict(zip(("stdout", "stderr"), ((None,) * 2 if V else (-1, 2))))
|
|
cmd = ["pip", "--python", str(env / "bin" / "python"), "install", *spec]
|
|
if (not no_st) and (not [x for x in spec if x.startswith("setuptools")]):
|
|
cmd.append("setuptools")
|
|
p = run(cmd, **kw)
|
|
if (rc := p.returncode) != 0:
|
|
if env.is_dir():
|
|
shutil.rmtree(env)
|
|
sys.stderr.write(f"pip had non zero exit ({rc})\n{p.stdout.decode()}\n")
|
|
sys.exit(rc)
|
|
meta.update(dict(id=_id, spec=spec, exe=exe, name=name, files=[runner]))
|
|
else:
|
|
meta = json.loads((env / "vivmeta.json").read_text())
|
|
meta.update(dict(accessed=t, files=sorted({*meta["files"], runner})))
|
|
(env / "vivmeta.json").write_text(json.dumps(meta))
|
|
site.addsitedir(sitepkgs := str(*(env / "lib").glob("py*/si*")))
|
|
sys.path = [sitepkgs, *filter(nopkgs, sys.path)]
|
|
|
|
return env
|
|
"""
|
|
|
|
@staticmethod
|
|
def description(name: str) -> str:
|
|
return f"""
|
|
|
|
{a.tagline()}
|
|
command line: `{a.bold}viv run typer rich-click -s ./script.py{a.end}`
|
|
python api: {a.style('__import__("viv").use("typer", "rich-click")','bold')}
|
|
"""
|
|
|
|
@staticmethod
|
|
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()))
|
|
|
|
@staticmethod
|
|
def _use_str(spec: List[str], standalone: bool = False) -> str:
|
|
spec_str = ", ".join(f'"{req}"' for req in spec)
|
|
if standalone:
|
|
return f"""_viv_use({fill(spec_str,width=90,subsequent_indent=" ",)})"""
|
|
else:
|
|
return f"""__import__("viv").use({spec_str})"""
|
|
|
|
@classmethod
|
|
def standalone(cls, spec: List[str]) -> str:
|
|
func_use = "\n".join(
|
|
(cls._standalone_func, cls.noqa(cls._use_str(spec, standalone=True)))
|
|
)
|
|
return f"""
|
|
# AUTOGENERATED by viv (v{__version__})
|
|
# see `python3 <(curl -fsSL viv.dayl.in/viv.py) --help`
|
|
|
|
{func_use}
|
|
"""
|
|
|
|
@staticmethod
|
|
def _rel_import(local_source: Optional[Path]) -> str:
|
|
if not local_source:
|
|
raise ValueError("local source must exist")
|
|
|
|
path_to_viv = path_to_viv = str(
|
|
local_source.resolve().absolute().parent
|
|
).replace(str(Path.home()), "~")
|
|
return (
|
|
"""__import__("sys").path.append(__import__("os")"""
|
|
f""".path.expanduser("{path_to_viv}")) # noqa"""
|
|
)
|
|
|
|
@staticmethod
|
|
def _absolute_import(local_source: Optional[Path]) -> str:
|
|
if not local_source:
|
|
raise ValueError("local source must exist")
|
|
|
|
path_to_viv = local_source.resolve().absolute().parent
|
|
return f"""__import__("sys").path.append("{path_to_viv}") # noqa"""
|
|
|
|
@classmethod
|
|
def frozen_import(
|
|
cls, path: str, local_source: Optional[Path], spec: List[str]
|
|
) -> str:
|
|
if path == "abs":
|
|
imports = cls._absolute_import(local_source)
|
|
elif path == "rel":
|
|
imports = cls._rel_import(local_source)
|
|
else:
|
|
imports = ""
|
|
return f"""\
|
|
{imports}
|
|
{cls.noqa(cls._use_str(spec))}
|
|
"""
|
|
|
|
@classmethod
|
|
def shim(
|
|
cls,
|
|
path: str,
|
|
local_source: Optional[Path],
|
|
standalone: bool,
|
|
spec: List[str],
|
|
bin: str,
|
|
) -> str:
|
|
if standalone:
|
|
imports = cls._standalone_func
|
|
elif path == "abs":
|
|
imports = cls._absolute_import(local_source)
|
|
elif path == "rel":
|
|
imports = cls._rel_import(local_source)
|
|
else:
|
|
imports = ""
|
|
return f"""\
|
|
#!/usr/bin/env python3
|
|
# AUTOGENERATED by viv (v{__version__})
|
|
# see `python3 <(curl -fsSL viv.dayl.in/viv.py) --help`
|
|
|
|
|
|
{imports}
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
if __name__ == "__main__":
|
|
vivenv = {cls.noqa(cls._use_str(spec, standalone))}
|
|
sys.exit(subprocess.run([vivenv / "bin" / "{bin}", *sys.argv[1:]]).returncode)
|
|
"""
|
|
|
|
@staticmethod
|
|
def update(
|
|
src: Optional[Path], cli: Path, local_version: str, next_version: str
|
|
) -> str:
|
|
return f"""
|
|
Update source at {a.green}{src}{a.end}
|
|
Symlink {a.bold}{src}{a.end} to {a.bold}{cli}{a.end}
|
|
Version {a.bold}{local_version}{a.end} -> {a.bold}{next_version}{a.end}
|
|
|
|
"""
|
|
|
|
@staticmethod
|
|
def install(src: Path, cli: Path) -> str:
|
|
return f"""
|
|
Install viv.py to {a.green}{src}{a.end}
|
|
Symlink {a.bold}{src}{a.end} to {a.bold}{cli}{a.end}
|
|
|
|
"""
|
|
|
|
|
|
# TODO: convert the below functions into a proper file/stream logging interface
|
|
def echo(
|
|
msg: str, style: str = "magenta", newline: bool = True, fd: TextIO = sys.stderr
|
|
) -> None:
|
|
"""output general message to stdout"""
|
|
output = f"{a.prefix} {a.sep} {msg}\n"
|
|
fd.write(output)
|
|
|
|
|
|
def error(*msg: str, exit: bool = True, code: int = 1) -> None:
|
|
"""output error message and if code provided exit"""
|
|
prefix = f"{a.prefix} {a.sep}{a.red}ERROR{a.end}{a.sep} "
|
|
sys.stderr.write("\n".join((f"{prefix}{line}" for line in msg)) + "\n")
|
|
if exit:
|
|
sys.exit(code)
|
|
|
|
|
|
def confirm(question: str, context: str = "", yes: bool = False) -> bool:
|
|
sys.stderr.write(context)
|
|
# TODO: update this
|
|
sys.stderr.write(
|
|
f"{a.prefix} {a.sep}{a.magenta}?{a.end}{a.sep}"
|
|
f" {question} {a.yellow}(Y)es/(n)o{a.end} "
|
|
)
|
|
if yes:
|
|
sys.stderr.write(f"{a.green}[FORCED YES]{a.end}\n")
|
|
return True
|
|
|
|
while True:
|
|
ans = input().strip().lower()
|
|
if ans in ("y", "yes"):
|
|
return True
|
|
elif ans in ("n", "no"):
|
|
return False
|
|
sys.stderr.write("Please select (Y)es or (n)o. ")
|
|
sys.stderr.write("\n")
|
|
|
|
|
|
class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
|
"""formatter to remove extra metavar on short opts"""
|
|
|
|
def _get_invocation_length(self, invocation: str) -> int:
|
|
return len(a.escape(invocation))
|
|
|
|
def _format_action_invocation(self, action: Action) -> str:
|
|
if not action.option_strings:
|
|
(metavar,) = self._metavar_formatter(action, action.dest)(1)
|
|
return a.style(metavar, style="option")
|
|
else:
|
|
parts = []
|
|
# if the Optional doesn't take a value, format is:
|
|
# -s, --long
|
|
if action.nargs == 0:
|
|
parts.extend(
|
|
[
|
|
a.style(option, style="option")
|
|
for option in action.option_strings
|
|
]
|
|
)
|
|
|
|
# if the Optional takes a value, format is:
|
|
# -s ARGS, --long ARGS
|
|
# change to
|
|
# -s, --long ARGS
|
|
else:
|
|
default = action.dest.upper()
|
|
args_string = self._format_args(action, default)
|
|
parts.extend(
|
|
[
|
|
a.style(option, style="option")
|
|
for option in action.option_strings
|
|
]
|
|
)
|
|
# add metavar to last string
|
|
parts[-1] += a.style(f" {args_string}", style="metavar")
|
|
return (", ").join(parts)
|
|
|
|
def _format_usage(self, *args: Any, **kwargs: Any) -> str:
|
|
formatted_usage = super()._format_usage(*args, **kwargs)
|
|
# patch usage with color formatting
|
|
formatted_usage = (
|
|
formatted_usage
|
|
if f"{a.header}usage{a.end}:" in formatted_usage
|
|
else formatted_usage.replace("usage:", f"{a.header}usage{a.end}:")
|
|
)
|
|
return formatted_usage
|
|
|
|
def _format_action(self, action: Action) -> str:
|
|
# determine the required width and the entry label
|
|
help_position = min(self._action_max_length + 2, self._max_help_position)
|
|
help_width = max(self._width - help_position, 11)
|
|
action_width = help_position - self._current_indent
|
|
action_header = self._format_action_invocation(action)
|
|
action_header_len = len(a.escape(action_header))
|
|
|
|
# no help; start on same line and add a final newline
|
|
if not action.help:
|
|
action_header = "%*s%s\n" % (self._current_indent, "", action_header)
|
|
# short action name; start on the same line and pad two spaces
|
|
elif action_header_len <= action_width:
|
|
# tup = self._current_indent, "", action_width, action_header
|
|
action_header = (
|
|
f"{' '*self._current_indent}{action_header}"
|
|
f"{' '*(action_width+2 - action_header_len)}"
|
|
)
|
|
indent_first = 0
|
|
|
|
# long action name; start on the next line
|
|
else:
|
|
action_header = "%*s%s\n" % (self._current_indent, "", action_header)
|
|
indent_first = help_position
|
|
|
|
# collect the pieces of the action help
|
|
parts = [action_header]
|
|
|
|
# if there was help for the action, add lines of help text
|
|
if action.help and action.help.strip():
|
|
help_text = self._expand_help(action)
|
|
if help_text:
|
|
help_lines = self._split_lines(help_text, help_width)
|
|
parts.append("%*s%s\n" % (indent_first, "", help_lines[0]))
|
|
for line in help_lines[1:]:
|
|
parts.append("%*s%s\n" % (help_position, "", line))
|
|
|
|
# or add a newline if the description doesn't end with one
|
|
elif not action_header.endswith("\n"):
|
|
parts.append("\n")
|
|
|
|
# if there are any sub-actions, add their help as well
|
|
for subaction in self._iter_indented_subactions(action):
|
|
parts.append(self._format_action(subaction))
|
|
|
|
# return a single string
|
|
return self._join_parts(parts)
|
|
|
|
def start_section(self, heading: Optional[str]) -> None:
|
|
if heading:
|
|
super().start_section(a.style(heading, style="header"))
|
|
else:
|
|
super()
|
|
|
|
def add_argument(self, action: Action) -> None:
|
|
if action.help is not SUPPRESS:
|
|
# find all invocations
|
|
get_invocation = self._format_action_invocation
|
|
invocations = [get_invocation(action)]
|
|
for subaction in self._iter_indented_subactions(action):
|
|
invocations.append(get_invocation(subaction))
|
|
|
|
# update the maximum item length accounting for ansi codes
|
|
invocation_length = max(map(self._get_invocation_length, invocations))
|
|
action_length = invocation_length + self._current_indent
|
|
self._action_max_length = max(self._action_max_length, action_length)
|
|
|
|
# add the item to the list
|
|
self._add_item(self._format_action, [action])
|
|
|
|
|
|
class KVAppendAction(Action):
|
|
def __init__(self, *args: Any, keys: List[str], **kwargs: Any) -> None:
|
|
self._keys = keys
|
|
super(KVAppendAction, self).__init__(*args, **kwargs)
|
|
|
|
def __call__(
|
|
self,
|
|
parser: StdArgParser,
|
|
namespace: Namespace,
|
|
values: Union[str, Sequence[Any], None],
|
|
option_string: str | None = None,
|
|
) -> None:
|
|
if not isinstance(values, str):
|
|
raise TypeError("expected string for `values`")
|
|
try:
|
|
(k, v) = values.split(":")
|
|
if k not in self._keys:
|
|
err_quit(
|
|
"".join(
|
|
(
|
|
f"unexpected key: {a.yellow}{k}{a.end} for {self.dest},",
|
|
" must be one of: ",
|
|
", ".join((a.style(k, "bold") for k in self._keys)),
|
|
)
|
|
)
|
|
)
|
|
d = {k: v}
|
|
except ValueError:
|
|
err_quit(
|
|
f"failed to parse key-value for {self.dest} "
|
|
f'"{a.bold}{values}{a.end}" as k:v'
|
|
)
|
|
items = getattr(namespace, self.dest)
|
|
if not items:
|
|
items = {}
|
|
|
|
items.update(d)
|
|
setattr(namespace, self.dest, items)
|
|
|
|
|
|
class ArgumentParser(StdArgParser):
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.formatter_class = lambda prog: CustomHelpFormatter(
|
|
prog,
|
|
max_help_position=35,
|
|
)
|
|
|
|
def error(self, message: str) -> NoReturn:
|
|
error(message, f"see `{a.bold}{self.prog} --help{a.end}` for more info")
|
|
sys.exit(2)
|
|
|
|
|
|
def subprocess_run(
|
|
command: List[str],
|
|
spinmsg: str = "",
|
|
clean_up_path: Optional[Path] = None,
|
|
verbose: bool = False,
|
|
ignore_error: bool = False,
|
|
check_output: bool = False,
|
|
) -> str:
|
|
"""run a subcommand
|
|
|
|
Args:
|
|
command: Subcommand to be run in subprocess.
|
|
verbose: If true, print subcommand output.
|
|
"""
|
|
|
|
log.debug("executing subcmd:\n " + " ".join(command))
|
|
|
|
if spinmsg and not verbose:
|
|
with Spinner(spinmsg):
|
|
p = subprocess.run(
|
|
command,
|
|
stdout=None if verbose else subprocess.PIPE,
|
|
stderr=None if verbose else subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
)
|
|
else:
|
|
p = subprocess.run(
|
|
command,
|
|
stdout=None if verbose else subprocess.PIPE,
|
|
stderr=None if verbose else subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
if p.returncode != 0 and not ignore_error:
|
|
a.subprocess(command, p.stdout)
|
|
|
|
if clean_up_path and clean_up_path.is_dir():
|
|
shutil.rmtree(str(clean_up_path))
|
|
|
|
sys.exit(p.returncode)
|
|
|
|
if not verbose:
|
|
log.debug(
|
|
"output:\n" + "\n".join(f"-> {line}" for line in p.stdout.splitlines())
|
|
)
|
|
|
|
if check_output:
|
|
return p.stdout
|
|
|
|
else:
|
|
return ""
|
|
|
|
|
|
def subprocess_run_quit(command: List[str | Path], **kwargs: Any) -> None:
|
|
log.debug("executing subcmd:\n " + " ".join(map(str, command)))
|
|
sys.exit(subprocess.run(command, **kwargs).returncode)
|
|
|
|
|
|
def get_hash(spec: Tuple[str, ...] | List[str], track_exe: bool = False) -> str:
|
|
"""generate a hash of package specifications
|
|
|
|
Args:
|
|
spec: sequence of package specifications
|
|
track_exe: if true add python executable to hash
|
|
Returns:
|
|
sha256 representation of dependencies for vivenv
|
|
"""
|
|
|
|
return hashlib.sha256(
|
|
(
|
|
str(spec) + (str(Path(sys.executable).resolve()) if track_exe else "N/A")
|
|
).encode()
|
|
).hexdigest()
|
|
|
|
|
|
def _get_user() -> str:
|
|
"""good-faith attempt to ascertain user name for viv cache"""
|
|
from getpass import getuser, GetPassWarning # noqa
|
|
|
|
try:
|
|
user = getuser()
|
|
except ImportError as e:
|
|
user = "dummy"
|
|
log.info(e.msg)
|
|
log.info("failed to get user with getpass.getuser", "using `dummy` as fallback")
|
|
return user
|
|
|
|
|
|
class Meta:
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
id: str,
|
|
spec: List[str],
|
|
files: List[str],
|
|
exe: str,
|
|
created: str = "",
|
|
accessed: str = "",
|
|
):
|
|
self.name = name
|
|
self.id = id
|
|
self.spec = spec
|
|
self.files = files
|
|
self.exe = exe
|
|
self.created = created
|
|
self.accessed = accessed
|
|
|
|
@classmethod
|
|
def load(cls, name: str) -> "Meta":
|
|
if not (Cfg().cache_venv / name / "vivmeta.json").exists():
|
|
log.warning(f"possibly corrupted vivenv: {name}")
|
|
# add empty values for corrupted vivenvs so it will still load
|
|
return cls(name=name, spec=[""], files=[""], exe="", id="")
|
|
else:
|
|
meta = json.loads((Cfg().cache_venv / name / "vivmeta.json").read_text())
|
|
|
|
return cls(**meta)
|
|
|
|
def write(self, p: Path | None = None) -> None:
|
|
if not p:
|
|
p = (Cfg().cache_venv) / self.name / "vivmeta.json"
|
|
|
|
p.write_text(json.dumps(self.__dict__))
|
|
|
|
def addfile(self, f: Path) -> None:
|
|
log.debug(f"associating {f} with {self.name}")
|
|
self.accessed = str(datetime.today())
|
|
self.files = sorted({*self.files, str(f.absolute().resolve())})
|
|
|
|
|
|
class ViVenv:
|
|
def __init__(
|
|
self,
|
|
spec: List[str] = [""],
|
|
track_exe: bool = False,
|
|
id: str | None = None,
|
|
name: str = "",
|
|
path: Path | None = None,
|
|
skip_load: bool = False,
|
|
metadata: Meta | None = None,
|
|
skip_validation: bool = False,
|
|
) -> None:
|
|
self.loaded = False
|
|
if not skip_validation:
|
|
spec = self._validate_spec(spec)
|
|
id = id if id else get_hash(spec, track_exe)
|
|
|
|
self.name = name if name else id[:8]
|
|
self.set_path(path)
|
|
|
|
if not metadata:
|
|
if self.name in (d.name for d in Cfg().cache_venv.iterdir()):
|
|
self.loaded = True
|
|
self.meta = Meta.load(self.name)
|
|
else:
|
|
self.meta = Meta(
|
|
spec=spec,
|
|
name=self.name,
|
|
id=id,
|
|
files=[],
|
|
exe=str(Path(sys.executable).resolve()) if track_exe else "N/A",
|
|
)
|
|
else:
|
|
self.meta = metadata
|
|
|
|
@classmethod
|
|
def load(cls, name: str) -> "ViVenv":
|
|
"""generate a vivenv from a vivmeta.json
|
|
Args:
|
|
name: used as lookup in the vivenv cache
|
|
"""
|
|
vivenv = cls(name=name, metadata=Meta.load(name))
|
|
|
|
return vivenv
|
|
|
|
def exists(self) -> None:
|
|
if self.name in (d.name for d in Cfg().cache_venv.iterdir()):
|
|
self.loaded = True
|
|
|
|
def set_path(self, path: Path | None = None) -> None:
|
|
self.path = path if path else Cfg().cache_venv / self.name
|
|
self.python = str(
|
|
(self.path / system.bin_dir / system.bin("python")).absolute()
|
|
)
|
|
self.pip = (system.bin("pip"), "--python", self.python)
|
|
|
|
def _validate_spec(self, spec: List[str]) -> List[str]:
|
|
"""ensure spec is at least of sequence of strings
|
|
|
|
Args:
|
|
spec: sequence of package specifications
|
|
"""
|
|
if not set(map(type, spec)) == {str}:
|
|
err_quit(
|
|
"unexepected input in package spec",
|
|
f"check your packages definitions: {spec}",
|
|
)
|
|
|
|
return sorted(spec)
|
|
|
|
def bin_exists(self, bin: str) -> None:
|
|
if not (self.path / system.bin_dir / bin).is_file():
|
|
message = f"{a.bold}{bin}{a.end} does not exist " "\nOptions:\n"
|
|
|
|
message += " " + " ".join(
|
|
(
|
|
a.style(p.name, "bold")
|
|
for p in (self.path / system.bin_dir).iterdir()
|
|
if not p.name.lower().startswith("activate")
|
|
)
|
|
)
|
|
err_quit(message)
|
|
|
|
def create(self, quiet: bool = False) -> None:
|
|
log.info(f"new unique vivenv: {a.bold}{self.name}{a.end}")
|
|
log.debug(f"creating new venv at {self.path}")
|
|
with Spinner("creating vivenv"):
|
|
venv.create(
|
|
self.path,
|
|
prompt=f"viv-{self.name}",
|
|
clear=True,
|
|
symlinks=platform.system() != "Windows",
|
|
)
|
|
|
|
self.meta.created = str(datetime.today())
|
|
|
|
def install_pkgs(self) -> None:
|
|
cmd: List[str] = [
|
|
*self.pip,
|
|
"install",
|
|
"--force-reinstall",
|
|
] + self.meta.spec
|
|
|
|
if not Env().viv_no_setuptools and "setuptools" not in self.meta.spec:
|
|
cmd.append("setuptools")
|
|
|
|
subprocess_run(
|
|
cmd,
|
|
spinmsg="installing packages in vivenv",
|
|
clean_up_path=self.path,
|
|
verbose=bool(Env().viv_verbose),
|
|
)
|
|
|
|
def ensure(self) -> None:
|
|
self.exists()
|
|
if not self.loaded or Env().viv_force:
|
|
self.create()
|
|
self.install_pkgs()
|
|
|
|
def touch(self) -> None:
|
|
self.meta.accessed = str(datetime.today())
|
|
|
|
@property
|
|
def site_packages(self) -> str:
|
|
return str(*(self.path / "lib").glob("python*/site-packages"))
|
|
|
|
def activate(self) -> None:
|
|
# also add sys.path here so that it comes first
|
|
log.debug(f"activating {self.name}")
|
|
path_to_add = self.site_packages
|
|
|
|
# approximate behavior of python -S
|
|
sys.path = [
|
|
path_to_add,
|
|
*(
|
|
p
|
|
for p in sys.path
|
|
if not p.endswith(("dist-packages", "site-packages"))
|
|
),
|
|
]
|
|
site.addsitedir(path_to_add)
|
|
|
|
def files_exist(self) -> bool:
|
|
return len([f for f in self.meta.files if Path(f).is_file()]) == 0
|
|
|
|
def get_size(self) -> None:
|
|
size = float(
|
|
sum(p.stat().st_size for p in Path(self.path).rglob("*") if p.is_file())
|
|
)
|
|
for unit in ("", "K", "M", "G", "T"):
|
|
if size < 1024:
|
|
break
|
|
size /= 1024
|
|
|
|
self.size = f"{size:.1f}{unit}B"
|
|
|
|
@contextmanager
|
|
def use(self, keep: bool = True) -> Generator[None, None, None]:
|
|
run_mode = Env().viv_run_mode
|
|
_path = self.path
|
|
|
|
def common() -> None:
|
|
self.ensure()
|
|
self.touch()
|
|
|
|
try:
|
|
if self.loaded or keep or run_mode == "persist":
|
|
common()
|
|
yield
|
|
elif run_mode == "ephemeral":
|
|
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
|
|
self.set_path(Path(tmpdir))
|
|
common()
|
|
yield
|
|
elif run_mode == "semi-ephemeral":
|
|
ephemeral_cache = _path_ok(
|
|
Path(tempfile.gettempdir()) / f"viv-ephemeral-cache-{_get_user()}"
|
|
)
|
|
os.environ.update(dict(VIV_CACHE=str(ephemeral_cache)))
|
|
self.set_path(ephemeral_cache / "venvs" / self.name)
|
|
common()
|
|
yield
|
|
finally:
|
|
self.set_path(_path)
|
|
|
|
def show(self, size_pad: int) -> None:
|
|
_id = (
|
|
self.meta.id[:8]
|
|
if self.meta.id == self.name
|
|
else (self.name[:5] + "..." if len(self.name) > 8 else self.name)
|
|
)
|
|
size = getattr(self, "size", None)
|
|
line = []
|
|
if size:
|
|
line.append(f"""{a.yellow}{size:>{size_pad}}{a.end}""")
|
|
|
|
line.extend(
|
|
(
|
|
f"""{a.bold}{a.cyan}{_id}{a.end}""",
|
|
f"""{a.style(", ".join(self.meta.spec),'dim')}""",
|
|
)
|
|
)
|
|
sys.stdout.write(" ".join(line) + "\n")
|
|
|
|
def _tree_leaves(self, items: List[str], indent: str = "") -> str:
|
|
tree_chars = ["├"] * (len(items) - 1) + ["╰"]
|
|
return "\n".join(
|
|
(f"{indent}{a.yellow}{c}─{a.end} {i}" for c, i in zip(tree_chars, items))
|
|
)
|
|
|
|
def tree(self) -> None:
|
|
items = [
|
|
f"{a.magenta}{k}{a.end}: {v}"
|
|
for k, v in {
|
|
**{
|
|
"id": self.meta.id,
|
|
"spec": ", ".join(self.meta.spec),
|
|
"created": self.meta.created,
|
|
"accessed": self.meta.accessed,
|
|
},
|
|
**(
|
|
{"size": getattr(self, "size", None)}
|
|
if getattr(self, "size", None)
|
|
else {}
|
|
),
|
|
**({"exe": self.meta.exe} if self.meta.exe != "N/A" else {}),
|
|
**({"files": ""} if self.meta.files else {}),
|
|
}.items()
|
|
]
|
|
rows = [f"\n{a.bold}{a.cyan}{self.name}{a.end}", self._tree_leaves(items)]
|
|
if self.meta.files:
|
|
rows += (self._tree_leaves(self.meta.files, indent=" "),)
|
|
|
|
sys.stdout.write("\n".join(rows) + "\n")
|
|
|
|
|
|
def get_caller_path() -> Path:
|
|
"""get callers callers file path"""
|
|
# viv.py is fist in stack since function is used in `viv.use()`
|
|
import inspect # noqa
|
|
|
|
frame_info = inspect.stack()[2]
|
|
filepath = frame_info.filename # in python 3.5+, you can use frame_info.filename
|
|
del frame_info # drop the reference to the stack frame to avoid reference cycles
|
|
|
|
return Path(filepath).absolute()
|
|
|
|
|
|
def use(*packages: str, track_exe: bool = False, name: str = "") -> Path:
|
|
"""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 id is used
|
|
"""
|
|
|
|
vivenv = ViVenv([*list(packages), *Env().viv_spec], track_exe=track_exe, name=name)
|
|
with vivenv.use():
|
|
vivenv.meta.addfile(get_caller_path())
|
|
vivenv.meta.write()
|
|
vivenv.activate()
|
|
return vivenv.path
|
|
|
|
|
|
def run() -> Path:
|
|
"""create a vivenv and append to sys.path using embedded metadata"""
|
|
source = get_caller_path().read_text()
|
|
metadata = _read_metadata_block(source)
|
|
deps = metadata.get("dependencies", [])
|
|
if requires := metadata.get("requires-python", ""):
|
|
_check_python(requires)
|
|
return use(*deps)
|
|
|
|
|
|
def combined_spec(reqs: List[str], requirements: Path) -> List[str]:
|
|
if requirements:
|
|
with requirements.open("r") as f:
|
|
reqs += f.readlines()
|
|
return reqs
|
|
|
|
|
|
def resolve_deps(reqs: List[str], requirements: Path) -> List[str]:
|
|
spec = combined_spec(reqs, requirements)
|
|
|
|
cmd = [
|
|
system.bin("pip"),
|
|
"install",
|
|
"--dry-run",
|
|
"--quiet",
|
|
"--ignore-installed",
|
|
"--disable-pip-version-check",
|
|
"--report",
|
|
"-",
|
|
] + spec
|
|
try:
|
|
result = subprocess_run(cmd, check_output=True, spinmsg="resolving depedencies")
|
|
report = json.loads(result)
|
|
except json.JSONDecodeError:
|
|
err_quit(
|
|
f"failed to parse result from cmd: {a.bold}{' '.join(cmd)}{a.end}\n"
|
|
"see viv log for output"
|
|
)
|
|
|
|
resolved_spec = [
|
|
f"{pkg['metadata']['name']}=={pkg['metadata']['version']}"
|
|
for pkg in report["install"]
|
|
]
|
|
|
|
return resolved_spec
|
|
|
|
|
|
def fetch_script(url: str) -> str:
|
|
from urllib.error import HTTPError # noqa
|
|
from urllib.request import urlopen # noqa
|
|
|
|
try:
|
|
log.debug(f"fetching from remote url: {url}")
|
|
r = urlopen(url)
|
|
except (HTTPError, ValueError) as e:
|
|
err_quit(
|
|
"Failed to fetch from remote url:",
|
|
f" {a.bold}{url}{a.end}",
|
|
"see below:" + a.style("-> ", "red").join(["\n"] + repr(e).splitlines()),
|
|
)
|
|
|
|
return r.read().decode("utf-8")
|
|
|
|
|
|
def fetch_source(reference: str) -> str:
|
|
src = fetch_script(
|
|
"https://raw.githubusercontent.com/daylinmorgan/viv/"
|
|
+ reference
|
|
+ "/src/viv/viv.py"
|
|
)
|
|
|
|
sha256 = hashlib.sha256(src.encode()).hexdigest()
|
|
|
|
cached_src_file = Cfg().cache_src / f"{sha256}.py"
|
|
|
|
if not cached_src_file.is_file():
|
|
log.debug("updating source script")
|
|
cached_src_file.write_text(src)
|
|
|
|
return sha256
|
|
|
|
|
|
def make_executable(path: Path) -> None:
|
|
"""apply an executable bit for all users with read access"""
|
|
mode = os.stat(path).st_mode
|
|
mode |= (mode & 0o444) >> 2 # copy R bits to X
|
|
os.chmod(path, mode)
|
|
|
|
|
|
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* # ignore comments/shebangs
|
|
(
|
|
(?:__import__\(\s*["']viv["']\s*\)\.)
|
|
|
|
|
(?:from\s+viv\s+import\s+)
|
|
|
|
|
(?:viv\.)
|
|
)
|
|
(?P<mode>(\w+))
|
|
""",
|
|
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 = (
|
|
r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
|
|
)
|
|
|
|
|
|
def _read_metadata_block(script: str) -> dict:
|
|
name = "script"
|
|
matches = list(
|
|
filter(lambda m: m.group("type") == name, re.finditer(METADATA_BLOCK, script))
|
|
)
|
|
if len(matches) > 1:
|
|
raise ValueError(f"Multiple {name} blocks found")
|
|
elif len(matches) == 1:
|
|
return toml_loads(
|
|
"\n".join((line[2:] for line in matches[0].group(0).splitlines()[1:-1]))
|
|
)
|
|
else:
|
|
return {}
|
|
|
|
|
|
def _check_python(requires: str):
|
|
version = Version(platform.python_version())
|
|
if version not in SpecifierSet(requires):
|
|
err_quit(
|
|
f"Running python {a.yellow}{version}{a.end} does "
|
|
f"not satisfy 'requires-python: {requires}'"
|
|
)
|
|
|
|
|
|
def _parse_date(txt: str) -> datetime:
|
|
"""attempt to parse datetime string
|
|
|
|
acceptable formats `%Y-%m-%d` & `%Y-%m-%dT%H:%M`
|
|
"""
|
|
|
|
try:
|
|
date = datetime.strptime(
|
|
txt,
|
|
"%Y-%m-%d",
|
|
)
|
|
return date
|
|
except ValueError:
|
|
pass
|
|
|
|
try:
|
|
date = datetime.strptime(txt, "%Y-%m-%dT%H:%M")
|
|
return date
|
|
except ValueError:
|
|
pass
|
|
|
|
err_quit(
|
|
f"failed to parse {a.yellow}{txt}{a.end} as datetime\n"
|
|
"acceptable formats `%Y-%m-%d` & `%Y-%m-%dT%H:%M`"
|
|
)
|
|
|
|
|
|
class Cache:
|
|
def __init__(self) -> None:
|
|
self.vivenvs = self._get_venvs()
|
|
|
|
def _get_venvs(self, cache_dir: Path = Cfg().cache_venv) -> Set[ViVenv]:
|
|
return {ViVenv.load(p.name) for p in cache_dir.iterdir()}
|
|
|
|
@staticmethod
|
|
def _compare_dates(
|
|
vivenv: ViVenv, date_name: str, when: str, date: datetime
|
|
) -> bool:
|
|
vivenv_date = datetime.strptime(
|
|
getattr(vivenv.meta, date_name), "%Y-%m-%d %H:%M:%S.%f"
|
|
)
|
|
if when == "before":
|
|
return vivenv_date < date
|
|
else:
|
|
return vivenv_date > date
|
|
|
|
def _filter_date(self, date_name: str, when: str, date: datetime) -> Set[ViVenv]:
|
|
return {
|
|
vivenv
|
|
for vivenv in self.vivenvs
|
|
if self._compare_dates(
|
|
vivenv,
|
|
date_name,
|
|
when,
|
|
date,
|
|
)
|
|
}
|
|
|
|
def _filter_file(self, file: str) -> Set[ViVenv]:
|
|
if file == "None":
|
|
return {vivenv for vivenv in self.vivenvs if vivenv.files_exist()}
|
|
else:
|
|
p = Path(file).absolute().resolve()
|
|
if not p.is_file():
|
|
err_quit(f"Unable to find local file: {file}")
|
|
return {vivenv for vivenv in self.vivenvs if str(p) in vivenv.meta.files}
|
|
|
|
def _filter_spec(self, spec: str) -> Set[ViVenv]:
|
|
return {
|
|
vivenv for vivenv in self.vivenvs if spec in ", ".join(vivenv.meta.spec)
|
|
}
|
|
|
|
def filter(self, filters: Dict[str, str]) -> Set[ViVenv]:
|
|
vivenv_sets = []
|
|
|
|
for k, v in filters.items():
|
|
if "-" in k: # date-based filters all have hyphen
|
|
(date_name, when) = k.split("-")
|
|
vivenv_sets.append(self._filter_date(date_name, when, _parse_date(v)))
|
|
elif k == "files":
|
|
vivenv_sets.append(self._filter_file(v))
|
|
|
|
elif k == "spec":
|
|
vivenv_sets.append(self._filter_spec(v))
|
|
|
|
if vivenv_sets:
|
|
return {vivenv for vivenv in set.union(*vivenv_sets)}
|
|
else:
|
|
return set()
|
|
|
|
|
|
class Viv:
|
|
def __init__(self) -> None:
|
|
self.t = Template()
|
|
self._cache = Cache()
|
|
self._get_sources()
|
|
self.name = "viv" if self.local else "python3 <(curl -fsSL viv.dayl.in/viv.py)"
|
|
|
|
def _get_sources(self) -> None:
|
|
self.local_source: Optional[Path] = None
|
|
self.running_source = Path(__file__).resolve()
|
|
self.local = not str(self.running_source).startswith("/proc/")
|
|
try:
|
|
# prevent running `viv` from being imported
|
|
curr = sys.path[0]
|
|
sys.path = sys.path[1:]
|
|
_local_viv = __import__("viv")
|
|
sys.path.insert(0, curr)
|
|
|
|
if _local_viv.__file__:
|
|
self.local_source = Path(_local_viv.__file__)
|
|
self.local_version = _local_viv.__version__
|
|
else:
|
|
self.local_version = "Not Found"
|
|
except ImportError:
|
|
self.local_version = "Not Found"
|
|
|
|
if self.local_source:
|
|
self.git = (self.local_source.parent.parent.parent / ".git").is_dir()
|
|
else:
|
|
self.git = False
|
|
|
|
def _match_vivenv(self, name_id: str) -> ViVenv: # type: ignore[return]
|
|
matches: List[ViVenv] = []
|
|
vivenvs = self._cache.vivenvs
|
|
|
|
for vivenv in vivenvs:
|
|
if name_id == vivenv.meta.id:
|
|
matches.append(vivenv)
|
|
elif vivenv.name == name_id:
|
|
matches.append(vivenv)
|
|
elif vivenv.name.startswith(name_id) or (
|
|
vivenv.meta.id.startswith(name_id) and vivenv.meta.id == vivenv.name
|
|
):
|
|
matches.append(vivenv)
|
|
elif vivenv.name.startswith(name_id):
|
|
matches.append(vivenv)
|
|
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
elif len(matches) > 1:
|
|
err_quit(
|
|
"matches: " + ",".join((match.name for match in matches)),
|
|
"too many matches maybe try a longer name?",
|
|
)
|
|
else:
|
|
err_quit(f"no matches found for {name_id}")
|
|
|
|
def cmd_env(self) -> None:
|
|
"""manage the viv vivenv cache"""
|
|
|
|
def cmd_env_remove(self, vivenvs: List[str]) -> None:
|
|
"""\
|
|
remove a vivenv
|
|
|
|
To remove all viv venvs:
|
|
`viv cache remove $(viv l -q)`
|
|
"""
|
|
|
|
for name in vivenvs:
|
|
vivenv = self._match_vivenv(name)
|
|
if vivenv.path.is_dir():
|
|
with Spinner(f"removing vivenv {a.bold}{vivenv.name}{a.end}"):
|
|
shutil.rmtree(vivenv.path)
|
|
log.info(f"{a.bold}{vivenv.name}{a.end} succesfully removed")
|
|
else:
|
|
err_quit(
|
|
f"cowardly exiting because I didn't find vivenv: {name}",
|
|
)
|
|
|
|
def cmd_freeze(
|
|
self,
|
|
reqs: List[str],
|
|
requirements: Path,
|
|
keep: bool,
|
|
standalone: bool,
|
|
path: str,
|
|
) -> None:
|
|
"""create import statement from package spec"""
|
|
|
|
spec = resolve_deps(reqs, requirements)
|
|
if keep:
|
|
vivenv = ViVenv(spec)
|
|
with vivenv.use():
|
|
vivenv.touch()
|
|
vivenv.meta.write()
|
|
|
|
log.info("see below for import statements\n")
|
|
|
|
if standalone:
|
|
sys.stdout.write(self.t.standalone(spec))
|
|
return
|
|
|
|
if path and not self.local_source:
|
|
err_quit("No local viv found to import from")
|
|
|
|
sys.stdout.write(self.t.frozen_import(path, self.local_source, spec))
|
|
|
|
def cmd_list(
|
|
self,
|
|
quiet: bool,
|
|
verbose: bool,
|
|
use_json: bool,
|
|
filter: Dict[str, str],
|
|
size: bool,
|
|
) -> None:
|
|
"""\
|
|
list vivenvs
|
|
|
|
examples:
|
|
`viv list \\
|
|
--filter "accessed-after:2023-08-01"`
|
|
`viv list -q --filter \\
|
|
"created-before:$(date -d '2 weeks ago' +'%Y-%m-%d')"`
|
|
`viv list --filter "files:./script.py"`
|
|
`viv list --filter "files:None"`
|
|
"""
|
|
|
|
if filter:
|
|
vivenvs = self._cache.filter(filter)
|
|
else:
|
|
vivenvs = self._cache.vivenvs
|
|
|
|
if quiet:
|
|
sys.stdout.write("\n".join((vivenv.meta.id for vivenv in vivenvs)) + "\n")
|
|
sys.exit(0)
|
|
|
|
# NOTE: this feels out of place
|
|
size_pad = 0
|
|
if size:
|
|
for vivenv in vivenvs:
|
|
vivenv.get_size()
|
|
size_pad = max(size_pad, len(vivenv.size))
|
|
|
|
if len(self._cache.vivenvs) == 0:
|
|
log.info("no vivenvs setup")
|
|
elif len(vivenvs) == 0 and filter:
|
|
log.info("no vivenvs match filter")
|
|
elif verbose:
|
|
for vivenv in vivenvs:
|
|
vivenv.tree()
|
|
elif use_json:
|
|
sys.stdout.write(
|
|
json.dumps({vivenv.name: vivenv.meta.__dict__ for vivenv in vivenvs})
|
|
)
|
|
else:
|
|
for vivenv in vivenvs:
|
|
vivenv.show(size_pad)
|
|
|
|
def cmd_env_exe(self, vivenv_id: str, cmd: str, rest: List[str]) -> None:
|
|
"""\
|
|
run binary/script in existing vivenv
|
|
|
|
examples:
|
|
viv exe <vivenv> python -- script.py
|
|
viv exe <vivenv> python -- -m http.server
|
|
"""
|
|
|
|
vivenv = self._match_vivenv(vivenv_id)
|
|
bin = vivenv.path / "bin" / cmd
|
|
vivenv.bin_exists(bin.name)
|
|
full_cmd = [str(bin), *rest]
|
|
|
|
# TODO: use subprocess_run_quit
|
|
subprocess_run(full_cmd, verbose=True)
|
|
|
|
def cmd_env_info(
|
|
self, vivenv_id: str, path: bool, use_json: bool, size: bool
|
|
) -> None:
|
|
"""get metadata about a vivenv"""
|
|
vivenv = self._match_vivenv(vivenv_id)
|
|
metadata_file = vivenv.path / "vivmeta.json"
|
|
|
|
if not metadata_file.is_file():
|
|
err_quit(f"Unable to find metadata for vivenv: {vivenv_id}")
|
|
if size:
|
|
vivenv.get_size()
|
|
if use_json:
|
|
sys.stdout.write(json.dumps(vivenv.meta.__dict__))
|
|
elif path:
|
|
sys.stdout.write(f"{vivenv.path.absolute()}\n")
|
|
else:
|
|
vivenv.tree()
|
|
|
|
def _install_local_src(self, sha256: str, src: Path, cli: Path, yes: bool) -> None:
|
|
log.info("updating local source copy of viv")
|
|
shutil.copy(Cfg().cache_src / f"{sha256}.py", src)
|
|
make_executable(src)
|
|
log.info("symlinking cli")
|
|
|
|
if cli.is_file():
|
|
log.info(f"Existing file at {a.style(str(cli),'bold')}")
|
|
if confirm("Would you like to overwrite it?", yes=yes):
|
|
cli.unlink()
|
|
cli.symlink_to(src)
|
|
else:
|
|
cli.parent.mkdir(exist_ok=True, parents=True)
|
|
cli.symlink_to(src)
|
|
|
|
log.info("Remember to include the following line in your shell rc file:")
|
|
sys.stderr.write(
|
|
' export PYTHONPATH="$PYTHONPATH:$HOME/'
|
|
f'{src.relative_to(Path.home()).parent}"\n'
|
|
)
|
|
|
|
def _get_new_version(self, ref: str) -> Tuple[str, str]:
|
|
sys.path.append(str(Cfg().cache_src))
|
|
return (sha256 := fetch_source(ref)), __import__(sha256).__version__
|
|
|
|
def cmd_manage(self) -> None:
|
|
"""manage viv itself"""
|
|
|
|
def cmd_manage_show(
|
|
self,
|
|
pythonpath: bool = False,
|
|
system: bool = False,
|
|
) -> None:
|
|
"""manage viv itself"""
|
|
if pythonpath:
|
|
if self.local and self.local_source:
|
|
sys.stdout.write(str(self.local_source.parent) + "\n")
|
|
else:
|
|
err_quit("expected to find a local installation")
|
|
else:
|
|
echo(f"{a.yellow}Current{a.end}:")
|
|
a.key_value(
|
|
{
|
|
"Version": __version__,
|
|
"CLI": shutil.which("viv"),
|
|
"Running Source": self.running_source,
|
|
"Local Source": self.local_source,
|
|
"Cache": Cfg().cache_base,
|
|
}
|
|
)
|
|
|
|
if system:
|
|
echo(f"{a.yellow}System{a.end}:")
|
|
a.key_value(
|
|
{
|
|
"Python Exe": sys.executable,
|
|
"Python Version": sys.version,
|
|
"Pip": subprocess_run(
|
|
["pip", "--version"], check_output=True
|
|
).strip(),
|
|
}
|
|
)
|
|
|
|
def cmd_manage_update(
|
|
self,
|
|
ref: str,
|
|
src: Path,
|
|
cli: Path,
|
|
yes: bool,
|
|
) -> None:
|
|
sha256, next_version = self._get_new_version(ref)
|
|
|
|
if self.local_version == next_version:
|
|
log.info(f"no change between {ref} and local version")
|
|
sys.exit(0)
|
|
|
|
if confirm(
|
|
"Would you like to perform the above installation steps?",
|
|
self.t.update(self.local_source, cli, self.local_version, next_version),
|
|
yes=yes,
|
|
):
|
|
self._install_local_src(
|
|
sha256,
|
|
Path(
|
|
src if not self.local_source else self.local_source,
|
|
),
|
|
cli,
|
|
yes,
|
|
)
|
|
|
|
def cmd_manage_install(
|
|
self,
|
|
ref: str,
|
|
src: Path,
|
|
cli: Path,
|
|
yes: bool,
|
|
) -> None:
|
|
sha256, downloaded_version = self._get_new_version(ref)
|
|
|
|
log.info(f"Downloaded version: {downloaded_version}")
|
|
|
|
# TODO: see if file is actually where
|
|
# we are about to install and give more instructions
|
|
|
|
if confirm(
|
|
"Would you like to perform the above installation steps?",
|
|
self.t.install(src, cli),
|
|
yes=yes,
|
|
):
|
|
self._install_local_src(sha256, src, cli, yes)
|
|
|
|
def cmd_manage_purge(
|
|
self,
|
|
ref: str,
|
|
src: Path,
|
|
cli: Path,
|
|
yes: bool,
|
|
) -> None:
|
|
to_remove = []
|
|
if Cfg().cache_base.is_dir():
|
|
to_remove.append(Cfg().cache_base)
|
|
if src.is_file():
|
|
to_remove.append(src.parent if src == (Cfg().src) else src)
|
|
if self.local_source and self.local_source.is_file():
|
|
if self.local_source.parent.name == "viv":
|
|
to_remove.append(self.local_source.parent)
|
|
else:
|
|
to_remove.append(self.local_source)
|
|
|
|
if cli.is_file():
|
|
to_remove.append(cli)
|
|
|
|
to_remove = list(set(to_remove))
|
|
if confirm(
|
|
"Remove the above files/directories?",
|
|
"\n".join(f" - {a.red}{p}{a.end}" for p in to_remove) + "\n",
|
|
yes=yes,
|
|
):
|
|
for p in to_remove:
|
|
if p.is_dir():
|
|
shutil.rmtree(p)
|
|
else:
|
|
p.unlink()
|
|
|
|
log.info(
|
|
"to re-install use: "
|
|
"`python3 <(curl -fsSL viv.dayl.in/viv.py) manage install`"
|
|
)
|
|
|
|
def _pick_bin(self, reqs: List[str], bin: str) -> Tuple[str, str]:
|
|
default = system.bin(re.split(r"[=><~!*]+", reqs[0])[0])
|
|
return default, (default if not bin else bin)
|
|
|
|
def cmd_shim(
|
|
self,
|
|
reqs: List[str],
|
|
requirements: Path,
|
|
bin: str,
|
|
output: Path,
|
|
freeze: bool,
|
|
yes: bool,
|
|
path: str,
|
|
standalone: bool,
|
|
) -> None:
|
|
"""\
|
|
generate viv-powered cli apps
|
|
|
|
examples:
|
|
viv shim black
|
|
viv shim yartsu -o ~/bin/yartsu --standalone
|
|
"""
|
|
|
|
default_bin, bin = self._pick_bin(reqs, bin)
|
|
output = Env().viv_bin_dir / default_bin if not output else output.absolute()
|
|
|
|
if freeze:
|
|
spec = resolve_deps(reqs, requirements)
|
|
else:
|
|
spec = combined_spec(reqs, requirements)
|
|
|
|
if output.is_file():
|
|
log.warning(f"{output} already exists")
|
|
|
|
if confirm(
|
|
f"Write shim for {a.bold}{bin}{a.end} to {a.green}{output}{a.end}?",
|
|
yes=yes,
|
|
):
|
|
output.parent.mkdir(exist_ok=True, parents=True)
|
|
with output.open("w") as f:
|
|
f.write(self.t.shim(path, self.local_source, standalone, spec, bin))
|
|
make_executable(output)
|
|
|
|
@staticmethod
|
|
def _update_cache(env: os._Environ[str], keep: bool, tmpdir: str) -> None:
|
|
run_mode = Env().viv_run_mode
|
|
if not keep:
|
|
if run_mode == "ephemeral":
|
|
new_cache = tmpdir
|
|
elif run_mode == "semi-ephemeral":
|
|
new_cache = str(
|
|
Path(tempfile.gettempdir()) / ("viv-ephemeral-cache-" + _get_user())
|
|
)
|
|
|
|
env.update({"VIV_CACHE": new_cache})
|
|
os.environ["VIV_CACHE"] = new_cache
|
|
|
|
def _run_script(
|
|
self, spec: List[str], script: str, keep: bool, rest: List[str]
|
|
) -> None:
|
|
env = os.environ
|
|
name = script.split("/")[-1]
|
|
|
|
with tempfile.TemporaryDirectory(prefix="viv-") as tmpdir:
|
|
tmppath = Path(tmpdir)
|
|
|
|
if Path(script).is_file():
|
|
scriptpath = Path(script).absolute()
|
|
script_text = scriptpath.read_text()
|
|
else:
|
|
scriptpath = tmppath / name
|
|
script_text = fetch_script(script)
|
|
scriptpath.write_text(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 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 mode != _Viv_Mode.NONE:
|
|
log.debug("fetching remote copy to use for python api")
|
|
(tmppath / "viv.py").write_text(
|
|
fetch_script(
|
|
"https://raw.githubusercontent.com/daylinmorgan/viv/latest/src/viv/viv.py"
|
|
)
|
|
)
|
|
|
|
self._update_cache(env, keep, tmpdir)
|
|
|
|
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],
|
|
env=dict(
|
|
env,
|
|
VIV_SPEC=" ".join(f"'{req}'" for req in spec),
|
|
PYTHONPATH=":".join((str(tmppath), env.get("PYTHONPATH", ""))),
|
|
),
|
|
)
|
|
elif mode == _Viv_Mode.RUN:
|
|
log.debug("script invokes viv.run letting subprocess handle deps")
|
|
subprocess_run_quit(
|
|
[sys.executable, "-S", scriptpath, *rest],
|
|
env=dict(
|
|
env,
|
|
PYTHONPATH=":".join((str(tmppath), env.get("PYTHONPATH", ""))),
|
|
),
|
|
)
|
|
elif not spec and not deps:
|
|
log.warning("using viv with empty spec, skipping vivenv creation")
|
|
subprocess_run_quit([sys.executable, "-S", scriptpath, *rest])
|
|
else:
|
|
vivenv = ViVenv(spec + deps)
|
|
with vivenv.use(keep=keep):
|
|
vivenv.meta.write()
|
|
subprocess_run_quit(
|
|
[vivenv.python, "-S", scriptpath, *rest],
|
|
env=dict(
|
|
env,
|
|
PYTHONPATH=":".join(
|
|
filter(None, (vivenv.site_packages, Env().pythonpath))
|
|
),
|
|
),
|
|
)
|
|
|
|
def cmd_run(
|
|
self,
|
|
reqs: List[str],
|
|
requirements: Path,
|
|
script: str,
|
|
keep: bool,
|
|
rest: List[str],
|
|
bin: str,
|
|
) -> None:
|
|
"""\
|
|
run an app/script with an on-demand venv
|
|
|
|
examples:
|
|
viv r pycowsay -- "viv isn't venv\\!"
|
|
viv r rich -b python -- -m rich
|
|
viv r -s <python script>
|
|
|
|
note: any args after `-s <python script>` will be passed on
|
|
"""
|
|
|
|
spec = combined_spec(reqs, requirements)
|
|
|
|
if script:
|
|
self._run_script(spec, script, keep, rest)
|
|
else:
|
|
_, bin = self._pick_bin(reqs, bin)
|
|
vivenv = ViVenv(spec)
|
|
|
|
with vivenv.use(keep=keep):
|
|
if keep or Env().viv_run_mode != "ephemeral":
|
|
vivenv.meta.write(vivenv.path / "vivmeta.json")
|
|
|
|
vivenv.bin_exists(bin)
|
|
subprocess_run_quit([vivenv.path / system.bin_dir / bin, *rest])
|
|
|
|
|
|
class Arg:
|
|
def __init__(self, *args: Any, flag: str | None = None, **kwargs: Any) -> None:
|
|
if flag:
|
|
self.args: Tuple[Any, ...] = (f"-{flag[0]}", f"--{flag}")
|
|
else:
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
|
|
|
|
class BoolArg(Arg):
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super(BoolArg, self).__init__(*args, action="store_true", **kwargs)
|
|
|
|
|
|
class PathArg(Arg):
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super(PathArg, self).__init__(*args, metavar="<path>", type=Path, **kwargs)
|
|
|
|
|
|
class Cli:
|
|
args: Dict[Tuple[str, ...], List[Arg]] = {
|
|
("list",): [
|
|
BoolArg(
|
|
flag="verbose",
|
|
help="pretty print full metadata for vivenvs",
|
|
),
|
|
BoolArg(
|
|
flag="quiet",
|
|
help="show only ids",
|
|
),
|
|
Arg(
|
|
flag="filter",
|
|
help="filter vivenvs based on key:val",
|
|
metavar="<key:value>",
|
|
action=KVAppendAction,
|
|
keys=[
|
|
"created-before",
|
|
"created-after",
|
|
"accessed-before",
|
|
"accessed-after",
|
|
"files",
|
|
"spec",
|
|
],
|
|
),
|
|
],
|
|
("shim",): [
|
|
BoolArg(
|
|
flag="freeze",
|
|
help="freeze/resolve all dependencies",
|
|
),
|
|
PathArg(
|
|
flag="output",
|
|
help="path/to/output file",
|
|
),
|
|
],
|
|
("env_info",): [
|
|
BoolArg(
|
|
flag="path",
|
|
help="print the absolute path to the vivenv",
|
|
),
|
|
],
|
|
("run",): [Arg(flag="script", help="script to execute", metavar="<path/url>")],
|
|
("env_exe", "env_info"): [
|
|
Arg("vivenv_id", help="name/hash of vivenv", metavar="vivenv")
|
|
],
|
|
("list", "env_info"): [
|
|
BoolArg(flag="size", help="calculate size of vivenvs"),
|
|
BoolArg(
|
|
"--json",
|
|
help="name:metadata json for vivenvs ",
|
|
default=False,
|
|
dest="use_json",
|
|
),
|
|
],
|
|
("freeze", "shim"): [
|
|
Arg(
|
|
flag="path",
|
|
help="generate line to add viv to sys.path",
|
|
choices=["abs", "rel"],
|
|
),
|
|
BoolArg(
|
|
flag="standalone",
|
|
help="generate standalone activation function",
|
|
),
|
|
],
|
|
("run", "freeze", "shim"): [
|
|
Arg("reqs", help="requirements specifiers", nargs="*"),
|
|
PathArg(
|
|
flag="requirements",
|
|
help="path/to/requirements.txt file",
|
|
),
|
|
],
|
|
("run", "freeze"): [
|
|
BoolArg(
|
|
flag="keep",
|
|
help="preserve environment",
|
|
),
|
|
],
|
|
("run", "shim"): [
|
|
Arg(flag="bin", help="console_script/script to invoke", metavar="<bin>"),
|
|
],
|
|
("manage_purge", "manage_update", "manage_install"): [
|
|
Arg(
|
|
flag="ref",
|
|
help="git reference (branch/tag/commit)",
|
|
default="latest",
|
|
metavar="<ref>",
|
|
),
|
|
PathArg(
|
|
flag="src",
|
|
help="path/to/source_file",
|
|
default=Cfg().src,
|
|
),
|
|
PathArg(
|
|
flag="cli",
|
|
help="path/to/cli (symlink to src)",
|
|
default=Path.home() / ".local" / "bin" / "viv",
|
|
),
|
|
],
|
|
("shim", "manage_purge", "manage_update", "manage_install"): [
|
|
BoolArg(flag="yes", help="respond yes to all prompts")
|
|
],
|
|
("manage_show",): [
|
|
BoolArg(
|
|
flag="pythonpath",
|
|
help="show the path/to/install",
|
|
),
|
|
BoolArg(flag="system", help="show system/python info too"),
|
|
],
|
|
("env_exe",): [
|
|
Arg(
|
|
"cmd",
|
|
help="command to to execute",
|
|
)
|
|
],
|
|
("env_remove",): [
|
|
Arg("vivenvs", help="name/hash of vivenv", nargs="*", metavar="vivenv")
|
|
],
|
|
}
|
|
(
|
|
cmds := dict.fromkeys(
|
|
(
|
|
"list",
|
|
"shim",
|
|
"run",
|
|
"env",
|
|
"freeze",
|
|
"manage",
|
|
)
|
|
)
|
|
).update(
|
|
{
|
|
cmd: {
|
|
subcmd: dict(description=help, help=help, aliases=[subcmd[0]])
|
|
for subcmd, help in subcmd_help
|
|
}
|
|
for cmd, subcmd_help in (
|
|
(
|
|
"env",
|
|
(
|
|
("exe", "run binary/script in existing vivenv"),
|
|
("info", "get metadata about a vivenv"),
|
|
("remove", "remove a vivenv"),
|
|
),
|
|
),
|
|
(
|
|
"manage",
|
|
(
|
|
("show", "show current installation"),
|
|
("install", "install fresh viv"),
|
|
("update", "update viv version"),
|
|
("purge", "remove traces of viv"),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
)
|
|
|
|
def __init__(self, viv: Viv) -> None:
|
|
self.viv = viv
|
|
self.parser = ArgumentParser(
|
|
prog=viv.name, description=viv.t.description(viv.name)
|
|
)
|
|
self._cmd_arg_group_map()
|
|
self.parsers = self._make_parsers()
|
|
self._add_args()
|
|
|
|
def _cmd_arg_group_map(self) -> None:
|
|
self.cmd_arg_group_map: Dict[str, List[Sequence[str] | str]] = {}
|
|
for grp in self.args:
|
|
if isinstance(grp, str):
|
|
self.cmd_arg_group_map.setdefault(grp, []).append(grp)
|
|
else:
|
|
for cmd in grp:
|
|
self.cmd_arg_group_map.setdefault(cmd, []).append(grp)
|
|
|
|
def _make_parsers(self) -> Dict[Sequence[str] | str, ArgumentParser]:
|
|
return {**{grp: ArgumentParser(add_help=False) for grp in self.args}}
|
|
|
|
def _add_args(self) -> None:
|
|
for grp, args in self.args.items():
|
|
for arg in args:
|
|
self.parsers[grp].add_argument(*arg.args, **arg.kwargs)
|
|
|
|
def _validate_args(self, args: Namespace) -> None:
|
|
name = args.func.__name__.replace("cmd_", "")
|
|
if name in ("freeze", "shim"):
|
|
if not args.reqs:
|
|
error("must specify a requirement")
|
|
|
|
if not self.viv.local_source and not (args.standalone or args.path):
|
|
log.warning(
|
|
"failed to find local copy of `viv` "
|
|
"make sure to add it to your PYTHONPATH "
|
|
"or consider using --path/--standalone"
|
|
)
|
|
|
|
# TODO: move this since this is code logic not flag logic
|
|
if args.path and not self.viv.local_source:
|
|
error("No local viv found to import from")
|
|
|
|
if args.path and args.standalone:
|
|
error("-p/--path and -s/--standalone are mutually exclusive")
|
|
|
|
if name == "manage_install" and self.viv.local_source:
|
|
error(
|
|
f"found existing viv installation at {self.viv.local_source}",
|
|
"use "
|
|
+ a.style("viv manage update", "bold")
|
|
+ " to modify current installation.",
|
|
)
|
|
|
|
if name == "manage_update":
|
|
if not self.viv.local_source:
|
|
error(
|
|
a.style("viv manage update", "bold")
|
|
+ " should be used with an exisiting installation",
|
|
)
|
|
|
|
if self.viv.git:
|
|
error(
|
|
a.style("viv manage update", "bold")
|
|
+ " shouldn't be used with a git-based installation",
|
|
)
|
|
|
|
if name in ("run", "shim"):
|
|
if not (args.reqs or args.script):
|
|
error("must specify a requirement or --script")
|
|
|
|
if name == "env_info":
|
|
if args.use_json and args.path:
|
|
error("--json and -p/--path are mutually exclusive")
|
|
|
|
def _get_subcmd_parser(
|
|
self,
|
|
subparsers: _SubParsersAction[ArgumentParser],
|
|
name: str,
|
|
attr: Optional[str] = None,
|
|
**kwargs: Any,
|
|
) -> ArgumentParser:
|
|
aliases = kwargs.pop("aliases", [name[0]])
|
|
|
|
cmd = getattr(self.viv, attr if attr else f"cmd_{name}")
|
|
|
|
parser: ArgumentParser = subparsers.add_parser(
|
|
name,
|
|
help=cmd.__doc__.splitlines()[0],
|
|
description=dedent(cmd.__doc__),
|
|
aliases=aliases,
|
|
**kwargs,
|
|
)
|
|
parser.set_defaults(func=cmd)
|
|
|
|
return parser
|
|
|
|
def run(self) -> None:
|
|
self.parser.add_argument(
|
|
"-V",
|
|
"--version",
|
|
action="version",
|
|
version=f"{a.bold}viv{a.end}, version {a.cyan}{__version__}{a.end}",
|
|
)
|
|
|
|
cmd_p = self.parser.add_subparsers(
|
|
metavar="<sub-cmd>", title="subcommands", required=True
|
|
)
|
|
|
|
for cmd, subcmds in self.cmds.items():
|
|
if subcmds:
|
|
subcmd_p = self._get_subcmd_parser(cmd_p, cmd)
|
|
subcmd_cmd_p = subcmd_p.add_subparsers(
|
|
title="subcommand",
|
|
metavar="<sub-cmd>",
|
|
required=True,
|
|
)
|
|
for subcmd, kwargs in subcmds.items():
|
|
subcmd_cmd_p.add_parser(
|
|
subcmd,
|
|
parents=[
|
|
self.parsers[k]
|
|
for k in self.cmd_arg_group_map[f"{cmd}_{subcmd}"]
|
|
],
|
|
**kwargs,
|
|
).set_defaults(func=getattr(self.viv, f"cmd_{cmd}_{subcmd}"))
|
|
|
|
else:
|
|
self._get_subcmd_parser(
|
|
cmd_p,
|
|
cmd,
|
|
parents=[self.parsers.get(k) for k in self.cmd_arg_group_map[cmd]],
|
|
)
|
|
if "--" in sys.argv:
|
|
i = sys.argv.index("--")
|
|
args = self.parser.parse_args(sys.argv[1:i])
|
|
args.rest = sys.argv[i + 1 :]
|
|
elif {"r", "run"} & set(sys.argv[1:2]) and (
|
|
flag := list({"-s", "--script"} & set(sys.argv))
|
|
):
|
|
i = sys.argv.index(flag[0])
|
|
args = self.parser.parse_args(sys.argv[1 : i + 2])
|
|
args.rest = sys.argv[i + 2 :]
|
|
else:
|
|
args = self.parser.parse_args()
|
|
if args.func.__name__ in ("cmd_run", "cmd_env_exe"):
|
|
args.rest = []
|
|
|
|
self._validate_args(args)
|
|
func = args.__dict__.pop("func")
|
|
func(
|
|
**vars(args),
|
|
)
|
|
|
|
|
|
def _no_traceback_excepthook(exc_type, exc_val, traceback):
|
|
# https://stackoverflow.com/questions/7073268/remove-traceback-in-python-on-ctrl-c
|
|
pass
|
|
|
|
|
|
def main() -> None:
|
|
try:
|
|
viv = Viv()
|
|
Cli(viv).run()
|
|
except KeyboardInterrupt:
|
|
echo(f"caught {a.bold}SIGINT")
|
|
if sys.excepthook is sys.__excepthook__:
|
|
sys.excepthook = _no_traceback_excepthook
|
|
raise
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|