evo-cli 0.4.0__tar.gz → 0.5.1__tar.gz
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.
- {evo_cli-0.4.0 → evo_cli-0.5.1}/PKG-INFO +1 -1
- evo_cli-0.5.1/evo_cli/VERSION +1 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/cli.py +2 -0
- evo_cli-0.5.1/evo_cli/commands/plantuml.py +753 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/SOURCES.txt +3 -1
- evo_cli-0.5.1/tests/test_plantuml.py +80 -0
- evo_cli-0.4.0/evo_cli/VERSION +0 -1
- {evo_cli-0.4.0 → evo_cli-0.5.1}/Containerfile +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/HISTORY.md +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/LICENSE +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/MANIFEST.in +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/README.md +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/__init__.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/__main__.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/base.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli/console.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/pyproject.toml +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/setup.cfg +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/tests/__init__.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/tests/test_cli.py +0 -0
- {evo_cli-0.4.0 → evo_cli-0.5.1}/tests/test_fix_claude.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.1
|
|
@@ -7,6 +7,7 @@ from evo_cli.commands.gdrive import gdrive
|
|
|
7
7
|
from evo_cli.commands.localproxy import localproxy
|
|
8
8
|
from evo_cli.commands.miniconda import miniconda
|
|
9
9
|
from evo_cli.commands.netcheck import netcheck
|
|
10
|
+
from evo_cli.commands.plantuml import plantuml
|
|
10
11
|
from evo_cli.commands.site2s import site2s
|
|
11
12
|
from evo_cli.commands.ssh import setupssh
|
|
12
13
|
|
|
@@ -39,6 +40,7 @@ cli.add_command(gdrive)
|
|
|
39
40
|
cli.add_command(site2s)
|
|
40
41
|
cli.add_command(localproxy)
|
|
41
42
|
cli.add_command(netcheck)
|
|
43
|
+
cli.add_command(plantuml)
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
def main():
|
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"""
|
|
2
|
+
evo plantuml - install, check and render PlantUML diagrams.
|
|
3
|
+
|
|
4
|
+
A self-contained PlantUML toolbox: it downloads the official plantuml.jar
|
|
5
|
+
(no system package needed), checks the runtime (Java + Graphviz), renders
|
|
6
|
+
.puml files to PNG/SVG/PDF and scaffolds vivid, ready-to-edit templates.
|
|
7
|
+
|
|
8
|
+
When Graphviz is missing, rendering falls back to PlantUML's pure-Java
|
|
9
|
+
Smetana layout engine so most diagrams still work with Java alone.
|
|
10
|
+
"""
|
|
11
|
+
import json as jsonlib
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import rich_click as click
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
from evo_cli.console import console, download_file, error, info, step, success, warning
|
|
30
|
+
|
|
31
|
+
GITHUB_LATEST = "https://api.github.com/repos/plantuml/plantuml/releases/latest"
|
|
32
|
+
GITHUB_TAG = "https://api.github.com/repos/plantuml/plantuml/releases/tags/{tag}"
|
|
33
|
+
FALLBACK_VERSION = "1.2026.6"
|
|
34
|
+
FALLBACK_URL = (
|
|
35
|
+
"https://github.com/plantuml/plantuml/releases/download/"
|
|
36
|
+
"v{ver}/plantuml-{ver}.jar"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
FORMATS = ["png", "svg", "pdf", "txt", "eps", "vdx", "latex"]
|
|
40
|
+
|
|
41
|
+
THEMES = [
|
|
42
|
+
"amiga", "aws-orange", "black-knight", "bluegray", "blueprint", "carbon-gray",
|
|
43
|
+
"cerulean", "cerulean-outline", "crt-amber", "crt-green", "cyborg",
|
|
44
|
+
"cyborg-outline", "hacker", "lightgray", "mars", "materia", "materia-outline",
|
|
45
|
+
"metal", "mimeograph", "minty", "mono", "plain", "reddress-darkblue",
|
|
46
|
+
"reddress-darkgreen", "reddress-darkorange", "reddress-darkred",
|
|
47
|
+
"reddress-lightblue", "reddress-lightgreen", "reddress-lightorange",
|
|
48
|
+
"reddress-lightred", "sandstone", "silver", "sketchy", "sketchy-outline",
|
|
49
|
+
"spacelab", "spacelab-white", "superhero", "superhero-outline", "toy",
|
|
50
|
+
"united", "vibrant",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
EPILOG = Text.from_markup(
|
|
54
|
+
"[bold]Examples[/bold]\n\n"
|
|
55
|
+
" [cyan]evo plantuml install[/cyan] download plantuml.jar + check runtime\n"
|
|
56
|
+
" [cyan]evo plantuml install --with-deps[/cyan] also install Java + Graphviz\n"
|
|
57
|
+
" [cyan]evo plantuml check[/cyan] verify Java / Graphviz / jar\n"
|
|
58
|
+
" [cyan]evo plantuml new sequence -o login.puml[/cyan] scaffold a vivid template\n"
|
|
59
|
+
" [cyan]evo plantuml render login.puml[/cyan] render to PNG next to the source\n"
|
|
60
|
+
" [cyan]evo plantuml render diagrams/ -f svg[/cyan] render a whole folder to SVG\n"
|
|
61
|
+
" [cyan]evo plantuml render login.puml -t cerulean --open[/cyan]\n"
|
|
62
|
+
" [cyan]evo plantuml render login.puml --watch[/cyan] re-render on every save\n"
|
|
63
|
+
" [cyan]evo plantuml themes[/cyan] list bundled themes\n\n"
|
|
64
|
+
"[dim]Diagrams that need layout (class, component, state, activity) use Graphviz when\n"
|
|
65
|
+
"present and fall back to the built-in Smetana engine otherwise.[/dim]"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def base_dir():
|
|
70
|
+
d = Path.home() / ".evo" / "plantuml"
|
|
71
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
return d
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def jar_path():
|
|
76
|
+
return base_dir() / "plantuml.jar"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def version_path():
|
|
80
|
+
return base_dir() / "version.txt"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def installed_version():
|
|
84
|
+
p = version_path()
|
|
85
|
+
if p.exists():
|
|
86
|
+
return p.read_text(encoding="utf-8").strip()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_java():
|
|
91
|
+
return shutil.which("java")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_dot():
|
|
95
|
+
return shutil.which("dot")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def java_version():
|
|
99
|
+
java = find_java()
|
|
100
|
+
if not java:
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
r = subprocess.run([java, "-version"], capture_output=True, text=True)
|
|
104
|
+
out = (r.stderr or r.stdout or "").strip().splitlines()
|
|
105
|
+
return out[0] if out else None
|
|
106
|
+
except OSError:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def dot_version():
|
|
111
|
+
dot = find_dot()
|
|
112
|
+
if not dot:
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
r = subprocess.run([dot, "-V"], capture_output=True, text=True)
|
|
116
|
+
return (r.stderr or r.stdout or "").strip() or None
|
|
117
|
+
except OSError:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def fetch_json(url):
|
|
122
|
+
req = urllib.request.Request(url, headers={"User-Agent": "evo-cli/plantuml"})
|
|
123
|
+
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
124
|
+
return jsonlib.loads(resp.read().decode("utf-8"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def pick_jar_asset(assets):
|
|
128
|
+
candidates = []
|
|
129
|
+
for a in assets:
|
|
130
|
+
name = a.get("name", "")
|
|
131
|
+
if not name.endswith(".jar"):
|
|
132
|
+
continue
|
|
133
|
+
if "javadoc" in name or "sources" in name:
|
|
134
|
+
continue
|
|
135
|
+
candidates.append(a)
|
|
136
|
+
exact = [a for a in candidates if re.match(r"^plantuml-\d.*\.jar$", a["name"])]
|
|
137
|
+
pool = exact or candidates
|
|
138
|
+
if not pool:
|
|
139
|
+
return None
|
|
140
|
+
pool.sort(key=lambda a: len(a["name"]))
|
|
141
|
+
return pool[0]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_download(release):
|
|
145
|
+
try:
|
|
146
|
+
url = GITHUB_LATEST if release in (None, "latest") else GITHUB_TAG.format(tag=release)
|
|
147
|
+
data = fetch_json(url)
|
|
148
|
+
tag = data.get("tag_name", "").lstrip("v")
|
|
149
|
+
asset = pick_jar_asset(data.get("assets", []))
|
|
150
|
+
if asset:
|
|
151
|
+
return asset["browser_download_url"], (tag or asset["name"])
|
|
152
|
+
except (urllib.error.URLError, OSError, ValueError, KeyError) as exc:
|
|
153
|
+
warning(f"GitHub API lookup failed ({exc}); using fallback {FALLBACK_VERSION}")
|
|
154
|
+
ver = FALLBACK_VERSION if release in (None, "latest") else release.lstrip("v")
|
|
155
|
+
return FALLBACK_URL.format(ver=ver), ver
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def detect_package_manager():
|
|
159
|
+
for mgr in ("apt-get", "dnf", "yum", "pacman", "zypper", "brew", "winget", "choco"):
|
|
160
|
+
if shutil.which(mgr):
|
|
161
|
+
return mgr
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def install_packages(packages, assume_yes):
|
|
166
|
+
mgr = detect_package_manager()
|
|
167
|
+
if not mgr:
|
|
168
|
+
warning("No supported package manager found. Install manually: " + ", ".join(packages))
|
|
169
|
+
return False
|
|
170
|
+
sudo = [] if (os.name == "nt" or mgr == "brew" or os.geteuid() == 0) else ["sudo"]
|
|
171
|
+
yes = ["-y"] if assume_yes else []
|
|
172
|
+
pkgmap = {
|
|
173
|
+
"java": {"apt-get": "default-jre", "dnf": "java-latest-openjdk", "yum": "java-latest-openjdk",
|
|
174
|
+
"pacman": "jre-openjdk", "zypper": "java-openjdk", "brew": "openjdk",
|
|
175
|
+
"winget": "EclipseAdoptium.Temurin.21.JRE", "choco": "temurin"},
|
|
176
|
+
"graphviz": {"apt-get": "graphviz", "dnf": "graphviz", "yum": "graphviz",
|
|
177
|
+
"pacman": "graphviz", "zypper": "graphviz", "brew": "graphviz",
|
|
178
|
+
"winget": "Graphviz.Graphviz", "choco": "graphviz"},
|
|
179
|
+
}
|
|
180
|
+
cmds = {
|
|
181
|
+
"apt-get": lambda p: sudo + ["apt-get", "install"] + yes + p,
|
|
182
|
+
"dnf": lambda p: sudo + ["dnf", "install"] + yes + p,
|
|
183
|
+
"yum": lambda p: sudo + ["yum", "install"] + yes + p,
|
|
184
|
+
"pacman": lambda p: sudo + ["pacman", "-S", "--noconfirm" if assume_yes else "--needed"] + p,
|
|
185
|
+
"zypper": lambda p: sudo + ["zypper", "install"] + yes + p,
|
|
186
|
+
"brew": lambda p: ["brew", "install"] + p,
|
|
187
|
+
"choco": lambda p: ["choco", "install"] + (["-y"] if assume_yes else []) + p,
|
|
188
|
+
}
|
|
189
|
+
resolved = [pkgmap[pkg][mgr] for pkg in packages if pkg in pkgmap and mgr in pkgmap[pkg]]
|
|
190
|
+
if not resolved:
|
|
191
|
+
warning(f"Don't know how to install {packages} with {mgr}.")
|
|
192
|
+
return False
|
|
193
|
+
if mgr == "apt-get":
|
|
194
|
+
subprocess.run(sudo + ["apt-get", "update"], check=False)
|
|
195
|
+
if mgr == "winget":
|
|
196
|
+
ok = True
|
|
197
|
+
for pkg in packages:
|
|
198
|
+
ident = pkgmap[pkg][mgr]
|
|
199
|
+
cmd = ["winget", "install", "-e", "--id", ident]
|
|
200
|
+
console.print(f"[cmd]$ {' '.join(cmd)}[/cmd]")
|
|
201
|
+
ok = subprocess.run(cmd).returncode == 0 and ok
|
|
202
|
+
return ok
|
|
203
|
+
cmd = cmds[mgr](resolved)
|
|
204
|
+
console.print(f"[cmd]$ {' '.join(cmd)}[/cmd]")
|
|
205
|
+
return subprocess.run(cmd).returncode == 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def run_jar(args, capture=True):
|
|
209
|
+
java = find_java()
|
|
210
|
+
if not java:
|
|
211
|
+
raise click.ClickException("Java not found. Run `evo plantuml install --with-deps` first.")
|
|
212
|
+
if not jar_path().exists():
|
|
213
|
+
raise click.ClickException("plantuml.jar not found. Run `evo plantuml install` first.")
|
|
214
|
+
cmd = [java, "-jar", str(jar_path())] + list(args)
|
|
215
|
+
return subprocess.run(cmd, capture_output=capture, text=True)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# install -------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def do_install(release, force, with_deps, assume_yes):
|
|
221
|
+
if with_deps:
|
|
222
|
+
missing = []
|
|
223
|
+
if not find_java():
|
|
224
|
+
missing.append("java")
|
|
225
|
+
if not find_dot():
|
|
226
|
+
missing.append("graphviz")
|
|
227
|
+
if missing:
|
|
228
|
+
info(f"Installing system packages: [accent]{', '.join(missing)}[/accent]")
|
|
229
|
+
install_packages(missing, assume_yes)
|
|
230
|
+
else:
|
|
231
|
+
info("Java and Graphviz already present.")
|
|
232
|
+
|
|
233
|
+
jar = jar_path()
|
|
234
|
+
if jar.exists() and not force:
|
|
235
|
+
warning(f"plantuml.jar already present at [accent]{jar}[/accent] "
|
|
236
|
+
f"(version {installed_version() or 'unknown'})")
|
|
237
|
+
info("Pass --force to re-download.")
|
|
238
|
+
else:
|
|
239
|
+
url, ver = resolve_download(release)
|
|
240
|
+
info(f"Downloading PlantUML [accent]{ver}[/accent]")
|
|
241
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
242
|
+
tmp_jar = Path(tmp) / "plantuml.jar"
|
|
243
|
+
try:
|
|
244
|
+
download_file(url, str(tmp_jar), "plantuml.jar")
|
|
245
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
246
|
+
raise click.ClickException(f"Download failed: {exc}")
|
|
247
|
+
shutil.move(str(tmp_jar), str(jar))
|
|
248
|
+
version_path().write_text(ver, encoding="utf-8")
|
|
249
|
+
success(f"Installed plantuml.jar ({ver}) at [accent]{jar}[/accent]")
|
|
250
|
+
|
|
251
|
+
report_environment()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def report_environment():
|
|
255
|
+
jv = java_version()
|
|
256
|
+
dv = dot_version()
|
|
257
|
+
console.print()
|
|
258
|
+
table = Table(show_header=True, header_style="accent", expand=False)
|
|
259
|
+
table.add_column("Component", style="info", no_wrap=True)
|
|
260
|
+
table.add_column("Status", no_wrap=True)
|
|
261
|
+
table.add_column("Detail", style="dim")
|
|
262
|
+
table.add_row("plantuml.jar",
|
|
263
|
+
"[success]ok[/success]" if jar_path().exists() else "[error]missing[/error]",
|
|
264
|
+
f"{jar_path()} ({installed_version() or '-'})")
|
|
265
|
+
table.add_row("Java", "[success]ok[/success]" if jv else "[error]missing[/error]",
|
|
266
|
+
jv or "needed to run PlantUML")
|
|
267
|
+
table.add_row("Graphviz (dot)", "[success]ok[/success]" if dv else "[warning]missing[/warning]",
|
|
268
|
+
dv or "optional - Smetana fallback will be used")
|
|
269
|
+
console.print(table)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# check ---------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
def do_check():
|
|
275
|
+
report_environment()
|
|
276
|
+
if not find_java() or not jar_path().exists():
|
|
277
|
+
error("PlantUML is not ready. Run `evo plantuml install --with-deps`.")
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
r = run_jar(["-version"])
|
|
281
|
+
out = (r.stdout or "") + (r.stderr or "")
|
|
282
|
+
for line in out.strip().splitlines()[:4]:
|
|
283
|
+
console.print(f"[dim]{line}[/dim]")
|
|
284
|
+
|
|
285
|
+
info("Testing Graphviz integration...")
|
|
286
|
+
rt = run_jar(["-testdot"])
|
|
287
|
+
test_out = ((rt.stdout or "") + (rt.stderr or "")).strip()
|
|
288
|
+
if "Error" in test_out or "cannot" in test_out.lower():
|
|
289
|
+
warning("Graphviz not usable by PlantUML; layout diagrams will use Smetana.")
|
|
290
|
+
for line in test_out.splitlines()[:4]:
|
|
291
|
+
console.print(f"[dim]{line}[/dim]")
|
|
292
|
+
else:
|
|
293
|
+
for line in test_out.splitlines()[:4]:
|
|
294
|
+
console.print(f"[dim]{line}[/dim]")
|
|
295
|
+
success("Graphviz works with PlantUML.")
|
|
296
|
+
|
|
297
|
+
step("Verdict")
|
|
298
|
+
if find_dot():
|
|
299
|
+
success("Ready to render every diagram type.")
|
|
300
|
+
else:
|
|
301
|
+
success("Ready to render (Graphviz missing: using Smetana for layout diagrams).")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# render --------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def collect_sources(target):
|
|
307
|
+
p = Path(target)
|
|
308
|
+
if p.is_dir():
|
|
309
|
+
files = sorted(set(p.rglob("*.puml")) | set(p.rglob("*.plantuml")) | set(p.rglob("*.pu")))
|
|
310
|
+
return [str(f) for f in files]
|
|
311
|
+
if p.exists():
|
|
312
|
+
return [str(p)]
|
|
313
|
+
raise click.ClickException(f"No such file or directory: {target}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_config(theme, engine):
|
|
317
|
+
lines = []
|
|
318
|
+
if engine == "smetana" or (engine == "auto" and not find_dot()):
|
|
319
|
+
lines.append("!pragma layout smetana")
|
|
320
|
+
if theme:
|
|
321
|
+
lines.append(f"!theme {theme}")
|
|
322
|
+
if not lines:
|
|
323
|
+
return None
|
|
324
|
+
fd, path = tempfile.mkstemp(suffix=".puml", prefix="evo-config-")
|
|
325
|
+
with os.fdopen(fd, "w", encoding="utf-8") as h:
|
|
326
|
+
h.write("\n".join(lines) + "\n")
|
|
327
|
+
return path
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def output_for(src, outdir, fmt):
|
|
331
|
+
src_path = Path(src)
|
|
332
|
+
ext = "tex" if fmt == "latex" else fmt
|
|
333
|
+
target_dir = Path(outdir) if outdir else src_path.parent
|
|
334
|
+
return target_dir / f"{src_path.stem}.{ext}"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def render_once(sources, fmt, outdir, config, scale, charset, extra):
|
|
338
|
+
args = [f"-t{fmt}", "-charset", charset]
|
|
339
|
+
if outdir:
|
|
340
|
+
args += ["-o", str(Path(outdir).resolve())]
|
|
341
|
+
if config:
|
|
342
|
+
args += ["-config", config]
|
|
343
|
+
if scale:
|
|
344
|
+
args += [f"-Sdpi={scale}"]
|
|
345
|
+
args += list(extra)
|
|
346
|
+
args += sources
|
|
347
|
+
r = run_jar(args, capture=True)
|
|
348
|
+
out = ((r.stdout or "") + (r.stderr or "")).strip()
|
|
349
|
+
return r.returncode, out
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def do_render(target, fmt, outdir, theme, engine, scale, charset, do_open, watch):
|
|
353
|
+
sources = collect_sources(target)
|
|
354
|
+
if not sources:
|
|
355
|
+
warning("No .puml / .plantuml / .pu files found.")
|
|
356
|
+
return
|
|
357
|
+
if outdir:
|
|
358
|
+
Path(outdir).mkdir(parents=True, exist_ok=True)
|
|
359
|
+
|
|
360
|
+
config = build_config(theme, engine)
|
|
361
|
+
extra = []
|
|
362
|
+
if engine == "smetana":
|
|
363
|
+
info("Layout engine: Smetana (pure Java)")
|
|
364
|
+
elif not find_dot():
|
|
365
|
+
info("Graphviz not found: using Smetana layout engine")
|
|
366
|
+
|
|
367
|
+
def _render():
|
|
368
|
+
code, out = render_once(sources, fmt, outdir, config, scale, charset, extra)
|
|
369
|
+
if out:
|
|
370
|
+
level = error if code != 0 else (warning if "Warning" in out else info)
|
|
371
|
+
for line in out.splitlines()[:8]:
|
|
372
|
+
level(line)
|
|
373
|
+
if code != 0:
|
|
374
|
+
error("Rendering failed.")
|
|
375
|
+
return False
|
|
376
|
+
for src in sources:
|
|
377
|
+
produced = output_for(src, outdir, fmt)
|
|
378
|
+
mark = "[success]ok[/success]" if produced.exists() else "[warning]?[/warning]"
|
|
379
|
+
console.print(f" {mark} {src} -> [accent]{produced}[/accent]")
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
ok = _render()
|
|
384
|
+
if ok:
|
|
385
|
+
success(f"Rendered {len(sources)} file(s) to {fmt.upper()}.")
|
|
386
|
+
if do_open and not watch:
|
|
387
|
+
open_file(output_for(sources[0], outdir, fmt))
|
|
388
|
+
|
|
389
|
+
if watch:
|
|
390
|
+
info("Watching for changes - Ctrl+C to stop.")
|
|
391
|
+
mtimes = {s: _mtime(s) for s in sources}
|
|
392
|
+
while True:
|
|
393
|
+
time.sleep(1)
|
|
394
|
+
for s in list(sources):
|
|
395
|
+
m = _mtime(s)
|
|
396
|
+
if m != mtimes.get(s):
|
|
397
|
+
mtimes[s] = m
|
|
398
|
+
console.print(f"[cmd]changed: {s}[/cmd]")
|
|
399
|
+
render_once([s], fmt, outdir, config, scale, charset, extra)
|
|
400
|
+
produced = output_for(s, outdir, fmt)
|
|
401
|
+
success(f"re-rendered -> {produced}")
|
|
402
|
+
except KeyboardInterrupt:
|
|
403
|
+
console.print()
|
|
404
|
+
info("Stopped watching.")
|
|
405
|
+
finally:
|
|
406
|
+
if config:
|
|
407
|
+
try:
|
|
408
|
+
os.unlink(config)
|
|
409
|
+
except OSError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _mtime(path):
|
|
414
|
+
try:
|
|
415
|
+
return os.path.getmtime(path)
|
|
416
|
+
except OSError:
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def open_file(path):
|
|
421
|
+
path = str(path)
|
|
422
|
+
try:
|
|
423
|
+
if os.name == "nt":
|
|
424
|
+
os.startfile(path) # noqa: S606
|
|
425
|
+
elif platform.system() == "Darwin":
|
|
426
|
+
subprocess.run(["open", path], check=False)
|
|
427
|
+
else:
|
|
428
|
+
subprocess.run(["xdg-open", path], check=False)
|
|
429
|
+
except OSError as exc:
|
|
430
|
+
warning(f"Could not open {path}: {exc}")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# new -----------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
TEMPLATES = {
|
|
436
|
+
"sequence": """@startuml
|
|
437
|
+
!theme cerulean
|
|
438
|
+
skinparam backgroundColor #FDFDFD
|
|
439
|
+
skinparam shadowing true
|
|
440
|
+
skinparam roundCorner 12
|
|
441
|
+
skinparam sequence {
|
|
442
|
+
ArrowColor #2C6FBB
|
|
443
|
+
LifeLineBorderColor #2C6FBB
|
|
444
|
+
ParticipantBorderColor #2C6FBB
|
|
445
|
+
ParticipantBackgroundColor #E8F1FB
|
|
446
|
+
}
|
|
447
|
+
title Login flow
|
|
448
|
+
|
|
449
|
+
actor User as user
|
|
450
|
+
participant "Web App" as web #E8F1FB
|
|
451
|
+
participant "Auth API" as auth #FFF3E0
|
|
452
|
+
database "DB" as db #E8F5E9
|
|
453
|
+
|
|
454
|
+
user -> web : open /login
|
|
455
|
+
web -> auth : POST /token
|
|
456
|
+
activate auth
|
|
457
|
+
auth -> db : verify credentials
|
|
458
|
+
db --> auth : user record
|
|
459
|
+
auth --> web : JWT
|
|
460
|
+
deactivate auth
|
|
461
|
+
web --> user : set cookie + redirect
|
|
462
|
+
note right of user : Session established
|
|
463
|
+
@enduml
|
|
464
|
+
""",
|
|
465
|
+
"class": """@startuml
|
|
466
|
+
!theme vibrant
|
|
467
|
+
skinparam shadowing true
|
|
468
|
+
skinparam roundCorner 10
|
|
469
|
+
skinparam classAttributeIconSize 0
|
|
470
|
+
title Domain model
|
|
471
|
+
|
|
472
|
+
class User {
|
|
473
|
+
+id: UUID
|
|
474
|
+
+email: String
|
|
475
|
+
--
|
|
476
|
+
+login(): Session
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
class Session {
|
|
480
|
+
+token: String
|
|
481
|
+
+expiresAt: DateTime
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
interface Repository<T> {
|
|
485
|
+
+get(id): T
|
|
486
|
+
+save(entity: T)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
class UserRepository
|
|
490
|
+
Repository <|.. UserRepository
|
|
491
|
+
User "1" o-- "many" Session : owns >
|
|
492
|
+
UserRepository ..> User : manages
|
|
493
|
+
@enduml
|
|
494
|
+
""",
|
|
495
|
+
"component": """@startuml
|
|
496
|
+
!theme aws-orange
|
|
497
|
+
skinparam shadowing true
|
|
498
|
+
skinparam roundCorner 12
|
|
499
|
+
title System components
|
|
500
|
+
|
|
501
|
+
package "Frontend" {
|
|
502
|
+
[Web App] as web
|
|
503
|
+
}
|
|
504
|
+
package "Backend" {
|
|
505
|
+
[API Gateway] as gw
|
|
506
|
+
[Auth Service] as auth
|
|
507
|
+
[Orders Service] as orders
|
|
508
|
+
}
|
|
509
|
+
database "PostgreSQL" as db
|
|
510
|
+
queue "Kafka" as bus
|
|
511
|
+
|
|
512
|
+
web --> gw : HTTPS
|
|
513
|
+
gw --> auth
|
|
514
|
+
gw --> orders
|
|
515
|
+
auth --> db
|
|
516
|
+
orders --> db
|
|
517
|
+
orders --> bus : events
|
|
518
|
+
@enduml
|
|
519
|
+
""",
|
|
520
|
+
"activity": """@startuml
|
|
521
|
+
!theme materia
|
|
522
|
+
skinparam shadowing true
|
|
523
|
+
title Checkout
|
|
524
|
+
|
|
525
|
+
start
|
|
526
|
+
:Add items to cart;
|
|
527
|
+
if (Logged in?) then (yes)
|
|
528
|
+
:Load saved address;
|
|
529
|
+
else (no)
|
|
530
|
+
:Prompt sign-in;
|
|
531
|
+
endif
|
|
532
|
+
:Review order;
|
|
533
|
+
repeat
|
|
534
|
+
:Try payment;
|
|
535
|
+
repeat while (Payment failed?) is (retry)
|
|
536
|
+
->success;
|
|
537
|
+
:Send confirmation email;
|
|
538
|
+
stop
|
|
539
|
+
@enduml
|
|
540
|
+
""",
|
|
541
|
+
"state": """@startuml
|
|
542
|
+
!theme superhero
|
|
543
|
+
skinparam shadowing true
|
|
544
|
+
title Order lifecycle
|
|
545
|
+
|
|
546
|
+
[*] --> Pending
|
|
547
|
+
Pending --> Paid : payment ok
|
|
548
|
+
Pending --> Cancelled : timeout
|
|
549
|
+
Paid --> Shipped : dispatch
|
|
550
|
+
Shipped --> Delivered : courier
|
|
551
|
+
Delivered --> [*]
|
|
552
|
+
Cancelled --> [*]
|
|
553
|
+
@enduml
|
|
554
|
+
""",
|
|
555
|
+
"usecase": """@startuml
|
|
556
|
+
!theme spacelab
|
|
557
|
+
skinparam shadowing true
|
|
558
|
+
left to right direction
|
|
559
|
+
title Shop use cases
|
|
560
|
+
|
|
561
|
+
actor Customer
|
|
562
|
+
actor Admin
|
|
563
|
+
|
|
564
|
+
rectangle Shop {
|
|
565
|
+
Customer --> (Browse catalog)
|
|
566
|
+
Customer --> (Place order)
|
|
567
|
+
Customer --> (Track order)
|
|
568
|
+
Admin --> (Manage products)
|
|
569
|
+
Admin --> (View reports)
|
|
570
|
+
}
|
|
571
|
+
@enduml
|
|
572
|
+
""",
|
|
573
|
+
"mindmap": """@startmindmap
|
|
574
|
+
!theme vibrant
|
|
575
|
+
* Project
|
|
576
|
+
** Backend
|
|
577
|
+
*** API
|
|
578
|
+
*** Database
|
|
579
|
+
** Frontend
|
|
580
|
+
*** Web
|
|
581
|
+
*** Mobile
|
|
582
|
+
** Ops
|
|
583
|
+
*** CI/CD
|
|
584
|
+
*** Monitoring
|
|
585
|
+
@endmindmap
|
|
586
|
+
""",
|
|
587
|
+
"gantt": """@startgantt
|
|
588
|
+
!theme cerulean
|
|
589
|
+
project starts 2026-01-06
|
|
590
|
+
[Design] lasts 5 days
|
|
591
|
+
[Build] lasts 10 days
|
|
592
|
+
[Build] starts at [Design]'s end
|
|
593
|
+
[Test] lasts 4 days
|
|
594
|
+
[Test] starts at [Build]'s end
|
|
595
|
+
[Launch] happens at [Test]'s end
|
|
596
|
+
@endgantt
|
|
597
|
+
""",
|
|
598
|
+
"er": """@startuml
|
|
599
|
+
!theme reddress-lightblue
|
|
600
|
+
skinparam shadowing true
|
|
601
|
+
title ER diagram
|
|
602
|
+
|
|
603
|
+
entity User {
|
|
604
|
+
*id : UUID <<PK>>
|
|
605
|
+
--
|
|
606
|
+
email : String
|
|
607
|
+
created_at : DateTime
|
|
608
|
+
}
|
|
609
|
+
entity Order {
|
|
610
|
+
*id : UUID <<PK>>
|
|
611
|
+
--
|
|
612
|
+
user_id : UUID <<FK>>
|
|
613
|
+
total : Decimal
|
|
614
|
+
}
|
|
615
|
+
entity Item {
|
|
616
|
+
*id : UUID <<PK>>
|
|
617
|
+
--
|
|
618
|
+
order_id : UUID <<FK>>
|
|
619
|
+
name : String
|
|
620
|
+
}
|
|
621
|
+
User ||--o{ Order
|
|
622
|
+
Order ||--|{ Item
|
|
623
|
+
@enduml
|
|
624
|
+
""",
|
|
625
|
+
"json": """@startjson
|
|
626
|
+
!theme vibrant
|
|
627
|
+
{
|
|
628
|
+
"user": "binhna",
|
|
629
|
+
"roles": ["admin", "dev"],
|
|
630
|
+
"active": true,
|
|
631
|
+
"limits": { "cpu": 4, "ram_gb": 16 }
|
|
632
|
+
}
|
|
633
|
+
@endjson
|
|
634
|
+
""",
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def do_new(kind, output, force, render, fmt):
|
|
639
|
+
if kind not in TEMPLATES:
|
|
640
|
+
raise click.ClickException(
|
|
641
|
+
f"Unknown template '{kind}'. Choose from: {', '.join(sorted(TEMPLATES))}"
|
|
642
|
+
)
|
|
643
|
+
out = Path(output) if output else Path(f"{kind}.puml")
|
|
644
|
+
if out.exists() and not force:
|
|
645
|
+
raise click.ClickException(f"{out} already exists. Pass --force to overwrite.")
|
|
646
|
+
out.write_text(TEMPLATES[kind], encoding="utf-8")
|
|
647
|
+
success(f"Created [accent]{out}[/accent] ({kind} template)")
|
|
648
|
+
info(f"Render it: [accent]evo plantuml render {out}[/accent]")
|
|
649
|
+
if render:
|
|
650
|
+
do_render(str(out), fmt, None, None, "auto", None, "UTF-8", True, False)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# command group -------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
@click.group("plantuml", epilog=EPILOG, context_settings={"help_option_names": ["-h", "--help"]})
|
|
656
|
+
def plantuml():
|
|
657
|
+
"""Install, check and render **PlantUML** diagrams.
|
|
658
|
+
|
|
659
|
+
Downloads the official `plantuml.jar` into `~/.evo/plantuml` (no system
|
|
660
|
+
package needed), checks the runtime (Java + Graphviz), renders `.puml`
|
|
661
|
+
files to PNG/SVG/PDF and scaffolds vivid templates with `evo plantuml new`.
|
|
662
|
+
|
|
663
|
+
Run `evo plantuml <command> -h` for the options of each subcommand.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@plantuml.command("install")
|
|
668
|
+
@click.option("-r", "--release", default="latest", show_default=True,
|
|
669
|
+
help="PlantUML release tag, e.g. `v1.2026.6`, or `latest`.")
|
|
670
|
+
@click.option("-f", "--force", is_flag=True, help="Re-download even if plantuml.jar exists.")
|
|
671
|
+
@click.option("--with-deps", is_flag=True, help="Also install Java and Graphviz via the system package manager.")
|
|
672
|
+
@click.option("-y", "--yes", "assume_yes", is_flag=True, help="Assume yes for package-manager prompts.")
|
|
673
|
+
def install_cmd(release, force, with_deps, assume_yes):
|
|
674
|
+
"""Download plantuml.jar and check the runtime.
|
|
675
|
+
|
|
676
|
+
The jar is fetched straight from the official GitHub releases. With
|
|
677
|
+
`--with-deps` it also installs **Java** and **Graphviz** using whichever
|
|
678
|
+
package manager it detects (apt, dnf, pacman, brew, winget, choco).
|
|
679
|
+
"""
|
|
680
|
+
step("evo plantuml install")
|
|
681
|
+
do_install(release, force, with_deps, assume_yes)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@plantuml.command("check")
|
|
685
|
+
def check_cmd():
|
|
686
|
+
"""Verify Java, Graphviz and plantuml.jar are working.
|
|
687
|
+
|
|
688
|
+
Prints the version of each component and runs PlantUML's own `-testdot`
|
|
689
|
+
so you know whether layout diagrams will use Graphviz or the Smetana
|
|
690
|
+
fallback.
|
|
691
|
+
"""
|
|
692
|
+
step("evo plantuml check")
|
|
693
|
+
do_check()
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@plantuml.command("render", epilog=EPILOG)
|
|
697
|
+
@click.argument("target", type=click.Path())
|
|
698
|
+
@click.option("-f", "--format", "fmt", type=click.Choice(FORMATS), default="png",
|
|
699
|
+
show_default=True, help="Output format.")
|
|
700
|
+
@click.option("-o", "--output", type=click.Path(), default=None,
|
|
701
|
+
help="Output directory (default: next to each source).")
|
|
702
|
+
@click.option("-t", "--theme", type=str, default=None, help="Apply a PlantUML theme to every diagram.")
|
|
703
|
+
@click.option("-e", "--engine", type=click.Choice(["auto", "dot", "smetana"]), default="auto",
|
|
704
|
+
show_default=True, help="Layout engine. `auto` uses Graphviz when available.")
|
|
705
|
+
@click.option("--scale", type=int, default=None, help="Output DPI (e.g. 150, 300) for raster formats.")
|
|
706
|
+
@click.option("--charset", default="UTF-8", show_default=True, help="Source charset.")
|
|
707
|
+
@click.option("--open", "do_open", is_flag=True, help="Open the first rendered file when done.")
|
|
708
|
+
@click.option("-w", "--watch", is_flag=True, help="Re-render automatically when sources change.")
|
|
709
|
+
def render_cmd(target, fmt, output, theme, engine, scale, charset, do_open, watch):
|
|
710
|
+
"""Render a `.puml` file or a directory of them.
|
|
711
|
+
|
|
712
|
+
`TARGET` may be a single file or a folder (rendered recursively, matching
|
|
713
|
+
`*.puml`, `*.plantuml`, `*.pu`). Use `-t` to apply a theme, `-w` to watch
|
|
714
|
+
and re-render on save, and `--open` to preview the result.
|
|
715
|
+
"""
|
|
716
|
+
step("evo plantuml render")
|
|
717
|
+
do_render(target, fmt, output, theme, engine, scale, charset, do_open, watch)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@plantuml.command("new")
|
|
721
|
+
@click.argument("kind", type=click.Choice(sorted(TEMPLATES)))
|
|
722
|
+
@click.option("-o", "--output", type=click.Path(), default=None, help="Output file (default: `<kind>.puml`).")
|
|
723
|
+
@click.option("-f", "--force", is_flag=True, help="Overwrite the file if it exists.")
|
|
724
|
+
@click.option("-r", "--render", is_flag=True, help="Render the new file right away.")
|
|
725
|
+
@click.option("--format", "fmt", type=click.Choice(FORMATS), default="png", show_default=True,
|
|
726
|
+
help="Format to use with --render.")
|
|
727
|
+
def new_cmd(kind, output, force, render, fmt):
|
|
728
|
+
"""Scaffold a vivid, ready-to-edit diagram template.
|
|
729
|
+
|
|
730
|
+
`KIND` is one of: sequence, class, component, activity, state, usecase,
|
|
731
|
+
mindmap, gantt, er, json. Each template ships with a theme and styling so
|
|
732
|
+
the output looks good immediately.
|
|
733
|
+
"""
|
|
734
|
+
step("evo plantuml new")
|
|
735
|
+
do_new(kind, output, force, render, fmt)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@plantuml.command("themes")
|
|
739
|
+
def themes_cmd():
|
|
740
|
+
"""List the built-in PlantUML themes you can pass to `render -t`."""
|
|
741
|
+
step("evo plantuml themes")
|
|
742
|
+
table = Table(show_header=False, expand=False, box=None)
|
|
743
|
+
cols = 3
|
|
744
|
+
for i in range(0, len(THEMES), cols):
|
|
745
|
+
table.add_row(*[f"[cyan]{t}[/cyan]" for t in THEMES[i:i + cols]])
|
|
746
|
+
console.print(table)
|
|
747
|
+
console.print()
|
|
748
|
+
info(f"{len(THEMES)} themes. Use: [accent]evo plantuml render diagram.puml -t cerulean[/accent]")
|
|
749
|
+
console.print(Panel(
|
|
750
|
+
"Themes are also set inside a file with `!theme <name>` on its own line "
|
|
751
|
+
"right after `@startuml`.",
|
|
752
|
+
border_style="info", expand=False,
|
|
753
|
+
))
|
|
@@ -23,8 +23,10 @@ evo_cli/commands/gdrive.py
|
|
|
23
23
|
evo_cli/commands/localproxy.py
|
|
24
24
|
evo_cli/commands/miniconda.py
|
|
25
25
|
evo_cli/commands/netcheck.py
|
|
26
|
+
evo_cli/commands/plantuml.py
|
|
26
27
|
evo_cli/commands/site2s.py
|
|
27
28
|
evo_cli/commands/ssh.py
|
|
28
29
|
tests/__init__.py
|
|
29
30
|
tests/test_cli.py
|
|
30
|
-
tests/test_fix_claude.py
|
|
31
|
+
tests/test_fix_claude.py
|
|
32
|
+
tests/test_plantuml.py
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from click.testing import CliRunner
|
|
2
|
+
|
|
3
|
+
from evo_cli.cli import cli
|
|
4
|
+
from evo_cli.commands import plantuml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_plantuml_registered():
|
|
8
|
+
assert "plantuml" in cli.commands
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_plantuml_help_runs():
|
|
12
|
+
result = CliRunner().invoke(cli, ["plantuml", "--help"])
|
|
13
|
+
assert result.exit_code == 0
|
|
14
|
+
for sub in ("install", "check", "render", "new", "themes"):
|
|
15
|
+
assert sub in result.output
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_pick_jar_asset_prefers_versioned_gpl_build():
|
|
19
|
+
assets = [
|
|
20
|
+
{"name": "plantuml-1.2026.6-javadoc.jar", "browser_download_url": "x"},
|
|
21
|
+
{"name": "plantuml-1.2026.6-sources.jar", "browser_download_url": "x"},
|
|
22
|
+
{"name": "plantuml-mit-1.2026.6.jar", "browser_download_url": "x"},
|
|
23
|
+
{"name": "plantuml-1.2026.6.jar", "browser_download_url": "good"},
|
|
24
|
+
{"name": "plantuml-1.2026.6.pdf", "browser_download_url": "x"},
|
|
25
|
+
]
|
|
26
|
+
chosen = plantuml.pick_jar_asset(assets)
|
|
27
|
+
assert chosen["name"] == "plantuml-1.2026.6.jar"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_resolve_download_fallback_on_failure(monkeypatch):
|
|
31
|
+
def boom(_url):
|
|
32
|
+
raise OSError("no network")
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(plantuml, "fetch_json", boom)
|
|
35
|
+
url, ver = plantuml.resolve_download("latest")
|
|
36
|
+
assert ver == plantuml.FALLBACK_VERSION
|
|
37
|
+
assert url.endswith(f"plantuml-{plantuml.FALLBACK_VERSION}.jar")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_templates_are_well_formed():
|
|
41
|
+
for kind, body in plantuml.TEMPLATES.items():
|
|
42
|
+
assert body.lstrip().startswith("@start")
|
|
43
|
+
assert "@end" in body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_new_creates_file(tmp_path):
|
|
47
|
+
out = tmp_path / "demo.puml"
|
|
48
|
+
result = CliRunner().invoke(cli, ["plantuml", "new", "sequence", "-o", str(out)])
|
|
49
|
+
assert result.exit_code == 0
|
|
50
|
+
assert out.exists()
|
|
51
|
+
assert out.read_text(encoding="utf-8").startswith("@startuml")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_new_rejects_existing_without_force(tmp_path):
|
|
55
|
+
out = tmp_path / "demo.puml"
|
|
56
|
+
out.write_text("keep", encoding="utf-8")
|
|
57
|
+
result = CliRunner().invoke(cli, ["plantuml", "new", "class", "-o", str(out)])
|
|
58
|
+
assert result.exit_code != 0
|
|
59
|
+
assert out.read_text(encoding="utf-8") == "keep"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_output_for_maps_extension(tmp_path):
|
|
63
|
+
src = tmp_path / "a.puml"
|
|
64
|
+
src.write_text("@startuml\n@enduml\n", encoding="utf-8")
|
|
65
|
+
assert plantuml.output_for(str(src), None, "svg").name == "a.svg"
|
|
66
|
+
assert plantuml.output_for(str(src), None, "latex").name == "a.tex"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_build_config_smetana(monkeypatch):
|
|
70
|
+
monkeypatch.setattr(plantuml, "find_dot", lambda: None)
|
|
71
|
+
path = plantuml.build_config("cerulean", "auto")
|
|
72
|
+
assert path is not None
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
content = open(path, encoding="utf-8").read()
|
|
77
|
+
assert "smetana" in content
|
|
78
|
+
assert "!theme cerulean" in content
|
|
79
|
+
finally:
|
|
80
|
+
os.unlink(path)
|
evo_cli-0.4.0/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.4.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|