skit-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skit/tokens.py ADDED
@@ -0,0 +1,112 @@
1
+ """Value tokens: run-time placeholders usable inside any form value.
2
+
3
+ `{cwd}` (invoke-time working directory), `{today}` (YYYY-MM-DD), `{now}` (HH-MM-SS),
4
+ `{env:NAME}` (environment variable), and a leading `~` (home). Values store the token
5
+ TEXT (intent), never the expanded result — argstate/presets persist `out_{today}.png`,
6
+ and every run expands fresh (the same rule that keeps `shots/*.png` a living glob).
7
+
8
+ Expansion contract:
9
+ - Only the known token names above are expanded. Any other `{...}` passes through
10
+ untouched — a value may legitimately contain braces destined for the script itself.
11
+ - `{{` and `}}` escape to literal `{` / `}` (the way to pass a literal `{cwd}` through).
12
+ - A missing environment variable is an error, never a silent empty string — a command
13
+ quietly assembled around "" is exactly the kind of breakage the non-interactive
14
+ contract forbids.
15
+
16
+ This module is headless and stdlib-only; `cwd`/`env`/`now` are injectable for tests.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import re
23
+ from collections.abc import Mapping
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ from .i18n import gettext
28
+
29
+ # {cwd} / {today} / {now} / {env:NAME}; NAME follows the usual environment-variable shape.
30
+ _TOKEN_RE = re.compile(r"\{(cwd|today|now|env:(?P<env>[A-Za-z_][A-Za-z0-9_]*))\}")
31
+
32
+
33
+ class TokenError(ValueError):
34
+ """A token cannot be expanded (currently: the named environment variable is unset)."""
35
+
36
+
37
+ def expand(
38
+ text: str,
39
+ *,
40
+ cwd: Path | str,
41
+ env: Mapping[str, str] | None = None,
42
+ now: datetime | None = None,
43
+ ) -> str:
44
+ """Expand tokens in text and return the final value. Raises TokenError."""
45
+ if env is None:
46
+ env = os.environ
47
+ if now is None:
48
+ now = datetime.now()
49
+ if text.startswith("~"):
50
+ text = os.path.expanduser(text)
51
+ out: list[str] = []
52
+ i = 0
53
+ n = len(text)
54
+ while i < n:
55
+ ch = text[i]
56
+ two = text[i : i + 2]
57
+ if two == "{{":
58
+ out.append("{")
59
+ i += 2
60
+ continue
61
+ if two == "}}":
62
+ out.append("}")
63
+ i += 2
64
+ continue
65
+ if ch == "{":
66
+ m = _TOKEN_RE.match(text, i)
67
+ if m is not None:
68
+ out.append(_resolve(m, cwd=cwd, env=env, now=now))
69
+ i = m.end()
70
+ continue
71
+ out.append(ch)
72
+ i += 1
73
+ return "".join(out)
74
+
75
+
76
+ def _resolve(m: re.Match[str], *, cwd: Path | str, env: Mapping[str, str], now: datetime) -> str:
77
+ name = m.group(1)
78
+ if name == "cwd":
79
+ return str(cwd)
80
+ if name == "today":
81
+ return now.strftime("%Y-%m-%d")
82
+ if name == "now":
83
+ return now.strftime("%H-%M-%S")
84
+ env_name = m.group("env")
85
+ if env_name not in env:
86
+ raise TokenError(
87
+ gettext("The environment variable %(name)s isn't set (needed by %(token)s).")
88
+ % {"name": env_name, "token": m.group(0)}
89
+ )
90
+ return env[env_name]
91
+
92
+
93
+ def preview(
94
+ text: str,
95
+ *,
96
+ cwd: Path | str,
97
+ env: Mapping[str, str] | None = None,
98
+ now: datetime | None = None,
99
+ ) -> tuple[str, str | None]:
100
+ """Non-raising expand for live form previews: (expanded, None) on success,
101
+ (original text, error message) when a token can't be expanded."""
102
+ try:
103
+ return expand(text, cwd=cwd, env=env, now=now), None
104
+ except TokenError as exc:
105
+ return text, str(exc)
106
+
107
+
108
+ def has_tokens(text: str) -> bool:
109
+ """Whether expand() would change text (used to decide if a preview line is worth showing)."""
110
+ return (
111
+ text.startswith("~") or "{{" in text or "}}" in text or _TOKEN_RE.search(text) is not None
112
+ )