tlaplus-cli 0.3.3__tar.gz → 0.3.5__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.
- {tlaplus_cli-0.3.3/src/tlaplus_cli.egg-info → tlaplus_cli-0.3.5}/PKG-INFO +11 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/README.md +10 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/pyproject.toml +1 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/build_tlc_module.py +16 -3
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/cli.py +2 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/config.py +21 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/run_tlc.py +9 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/settings.py +1 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/tools_manager.py +21 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/version_manager.py +118 -2
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5/src/tlaplus_cli.egg-info}/PKG-INFO +11 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/SOURCES.txt +5 -8
- tlaplus_cli-0.3.5/tests/test_build_modules_path.py +50 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/tests/test_cli.py +1 -4
- tlaplus_cli-0.3.5/tests/test_config.py +99 -0
- tlaplus_cli-0.3.5/tests/test_modules_path.py +62 -0
- tlaplus_cli-0.3.5/tests/test_settings.py +38 -0
- tlaplus_cli-0.3.5/tests/test_tlc_modules_path.py +43 -0
- tlaplus_cli-0.3.3/tests/test_build_tlc_module.py +0 -161
- tlaplus_cli-0.3.3/tests/test_check_java.py +0 -69
- tlaplus_cli-0.3.3/tests/test_java_module_naming.py +0 -86
- tlaplus_cli-0.3.3/tests/test_project.py +0 -50
- tlaplus_cli-0.3.3/tests/test_run_tlc.py +0 -96
- tlaplus_cli-0.3.3/tests/test_spec_resolution.py +0 -200
- tlaplus_cli-0.3.3/tests/test_tools_manager.py +0 -636
- tlaplus_cli-0.3.3/tests/test_version_manager.py +0 -193
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/LICENSE +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/MANIFEST.in +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/setup.cfg +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/__init__.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/check_java.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/project.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/py.typed +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/resources/__init__.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli/resources/default_config.yaml +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/requires.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tlaplus-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: TLA+ tools: download TLC, compile custom modules, run model checker
|
|
5
5
|
Author-email: Denis Nikolskiy <codeomatics@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -170,6 +170,14 @@ Compile modules in a specific project path:
|
|
|
170
170
|
tla modules build path/to/project
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
Configure a persistent custom modules path:
|
|
174
|
+
```bash
|
|
175
|
+
tla modules path /path/to/custom/modules
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> [!TIP]
|
|
179
|
+
> Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
|
|
180
|
+
|
|
173
181
|
Verbose output:
|
|
174
182
|
```bash
|
|
175
183
|
tla modules build --verbose
|
|
@@ -221,6 +229,8 @@ tlc:
|
|
|
221
229
|
java_class: tlc2.TLC
|
|
222
230
|
overrides_class: tlc2.overrides.TLCOverrides
|
|
223
231
|
|
|
232
|
+
module_path: null # (Optional) Persistent custom modules path
|
|
233
|
+
|
|
224
234
|
java:
|
|
225
235
|
min_version: 11
|
|
226
236
|
opts:
|
|
@@ -141,6 +141,14 @@ Compile modules in a specific project path:
|
|
|
141
141
|
tla modules build path/to/project
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
Configure a persistent custom modules path:
|
|
145
|
+
```bash
|
|
146
|
+
tla modules path /path/to/custom/modules
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> [!TIP]
|
|
150
|
+
> Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
|
|
151
|
+
|
|
144
152
|
Verbose output:
|
|
145
153
|
```bash
|
|
146
154
|
tla modules build --verbose
|
|
@@ -192,6 +200,8 @@ tlc:
|
|
|
192
200
|
java_class: tlc2.TLC
|
|
193
201
|
overrides_class: tlc2.overrides.TLCOverrides
|
|
194
202
|
|
|
203
|
+
module_path: null # (Optional) Persistent custom modules path
|
|
204
|
+
|
|
195
205
|
java:
|
|
196
206
|
min_version: 11
|
|
197
207
|
opts:
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from tlaplus_cli.config import cache_dir, load_config, workspace_root
|
|
9
|
+
from tlaplus_cli.config import cache_dir, load_config, save_config, workspace_root
|
|
10
10
|
from tlaplus_cli.version_manager import get_pinned_version_dir
|
|
11
11
|
|
|
12
12
|
|
|
@@ -28,9 +28,8 @@ def build(
|
|
|
28
28
|
typer.echo("Error: tla2tools.jar not found.\nRun 'tla tools install' first.", err=True)
|
|
29
29
|
raise typer.Exit(1)
|
|
30
30
|
|
|
31
|
-
modules_dir = base_dir / config.workspace.modules_dir
|
|
31
|
+
modules_dir = Path(config.module_path) if config.module_path else base_dir / config.workspace.modules_dir
|
|
32
32
|
classes_dir = base_dir / config.workspace.classes_dir
|
|
33
|
-
|
|
34
33
|
lib_dir = base_dir / "lib"
|
|
35
34
|
lib_jars = sorted(lib_dir.glob("*.jar")) if lib_dir.is_dir() else []
|
|
36
35
|
classpath = os.pathsep.join([str(jar_path)] + [str(j) for j in lib_jars])
|
|
@@ -71,3 +70,17 @@ def build(
|
|
|
71
70
|
except FileNotFoundError:
|
|
72
71
|
typer.echo("Error: 'javac' not found. Ensure JDK is installed and in PATH.", err=True)
|
|
73
72
|
raise typer.Exit(1) from None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_modules_path(path: str = typer.Argument(..., help="Path to the custom Java modules directory.")) -> None:
|
|
76
|
+
"""Configure a persistent custom Java modules path."""
|
|
77
|
+
config_obj = load_config()
|
|
78
|
+
p = Path(path).resolve()
|
|
79
|
+
|
|
80
|
+
if not p.is_dir():
|
|
81
|
+
typer.echo(f"Error: Path does not exist or is not a directory: {p}", err=True)
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
config_obj.module_path = str(p)
|
|
85
|
+
save_config(config_obj)
|
|
86
|
+
typer.echo(f"Modules path updated to: {p}")
|
|
@@ -5,6 +5,7 @@ import importlib.metadata
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from tlaplus_cli.build_tlc_module import build as build_tlc_cmd
|
|
8
|
+
from tlaplus_cli.build_tlc_module import set_modules_path
|
|
8
9
|
from tlaplus_cli.check_java import check_java_version, get_java_version
|
|
9
10
|
from tlaplus_cli.config import load_config
|
|
10
11
|
from tlaplus_cli.run_tlc import tlc as run_tlc_cmd
|
|
@@ -50,6 +51,7 @@ def root(
|
|
|
50
51
|
# Attach typer subgroups
|
|
51
52
|
modules_app = typer.Typer(name="modules", help="Manage TLA+ Java modules.", no_args_is_help=True)
|
|
52
53
|
modules_app.command(name="build")(build_tlc_cmd)
|
|
54
|
+
modules_app.command(name="path")(set_modules_path)
|
|
53
55
|
|
|
54
56
|
app.add_typer(modules_app, name="modules")
|
|
55
57
|
app.add_typer(tools_app, name="tools")
|
|
@@ -17,11 +17,31 @@ def load_config() -> Settings:
|
|
|
17
17
|
Creates a default config on first run.
|
|
18
18
|
"""
|
|
19
19
|
cp = _ensure_config()
|
|
20
|
-
with cp.open() as f:
|
|
20
|
+
with cp.open(encoding="utf-8") as f:
|
|
21
21
|
data = yaml.safe_load(f)
|
|
22
22
|
return Settings.model_validate(data)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def save_config(settings: Settings) -> None:
|
|
26
|
+
"""Save config to the user config directory.
|
|
27
|
+
|
|
28
|
+
Uses a defensive deep copy of settings.
|
|
29
|
+
"""
|
|
30
|
+
cp = config_path()
|
|
31
|
+
# defensive deep copy as per plan
|
|
32
|
+
base_settings = settings.model_copy(deep=True)
|
|
33
|
+
|
|
34
|
+
# Convert to dict, excluding None values for a cleaner config if desired,
|
|
35
|
+
# but the plan doesn't specify exclusion. Pydantic's model_dump is good.
|
|
36
|
+
data = base_settings.model_dump(mode="json")
|
|
37
|
+
|
|
38
|
+
with cp.open("w", encoding="utf-8") as f:
|
|
39
|
+
yaml.safe_dump(data, f, default_flow_style=False)
|
|
40
|
+
|
|
41
|
+
# Clear cache to ensure subsequent loads get the new data
|
|
42
|
+
load_config.cache_clear()
|
|
43
|
+
|
|
44
|
+
|
|
25
45
|
def config_dir() -> Path:
|
|
26
46
|
"""OS-standard user config directory for this app."""
|
|
27
47
|
return Path(platformdirs.user_config_dir(_APP_NAME))
|
|
@@ -85,6 +85,15 @@ def tlc(
|
|
|
85
85
|
|
|
86
86
|
classpath_parts = [str(jar_path)]
|
|
87
87
|
extra_jvm_opts: list[str] = []
|
|
88
|
+
|
|
89
|
+
if config.module_path:
|
|
90
|
+
custom_path = Path(config.module_path)
|
|
91
|
+
if custom_path.is_dir():
|
|
92
|
+
# Phase 4: safely append the custom directory path to the Java -cp mechanism
|
|
93
|
+
classpath_parts.append(str(custom_path))
|
|
94
|
+
# Also add to TLA-Library for convenience if TLA modules are there
|
|
95
|
+
extra_jvm_opts.append(f"-DTLA-Library={custom_path}")
|
|
96
|
+
|
|
88
97
|
if project_root:
|
|
89
98
|
classes_path = project_root / config.workspace.classes_dir
|
|
90
99
|
if classes_path.is_dir():
|
|
@@ -12,9 +12,11 @@ from tlaplus_cli.version_manager import (
|
|
|
12
12
|
clear_cache,
|
|
13
13
|
clear_pin,
|
|
14
14
|
download_version,
|
|
15
|
+
download_version_from_url,
|
|
15
16
|
fetch_remote_versions,
|
|
16
17
|
get_pinned_version_dir,
|
|
17
18
|
get_tools_dir,
|
|
19
|
+
is_url,
|
|
18
20
|
list_local_versions,
|
|
19
21
|
read_version_metadata,
|
|
20
22
|
resolve_latest_version,
|
|
@@ -100,6 +102,25 @@ def install(
|
|
|
100
102
|
version: str = typer.Argument(None),
|
|
101
103
|
force: bool = typer.Option(False, "--force", "-f", help="Force re-download even if already installed."),
|
|
102
104
|
) -> None:
|
|
105
|
+
# --- URL install branch ---
|
|
106
|
+
if version and is_url(version):
|
|
107
|
+
try:
|
|
108
|
+
version_dir = download_version_from_url(version)
|
|
109
|
+
typer.echo("Download complete.")
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
typer.echo(f"Error: {e}", err=True)
|
|
112
|
+
raise typer.Exit(1) from e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
typer.echo(f"Error: Failed to download: {e}", err=True)
|
|
115
|
+
raise typer.Exit(1) from e
|
|
116
|
+
|
|
117
|
+
pinned_dir = get_pinned_version_dir()
|
|
118
|
+
if pinned_dir is None:
|
|
119
|
+
typer.echo(f"Auto-pinning newly installed version {version_dir.name}")
|
|
120
|
+
set_pin(version_dir)
|
|
121
|
+
return
|
|
122
|
+
# --- end URL branch ---
|
|
123
|
+
|
|
103
124
|
config = load_config()
|
|
104
125
|
versions, _ = fetch_remote_versions(config.tla.urls.tags, config.tla.urls.releases, config.tla.urls.per_page)
|
|
105
126
|
if not versions:
|
|
@@ -6,8 +6,10 @@ import shutil
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
8
|
from dataclasses import asdict, dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
11
13
|
|
|
12
14
|
import requests
|
|
13
15
|
import typer
|
|
@@ -22,6 +24,118 @@ if TYPE_CHECKING:
|
|
|
22
24
|
_SEMVER_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)")
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def is_url(text: str) -> bool:
|
|
28
|
+
"""Return True if *text* looks like an HTTP(S) URL."""
|
|
29
|
+
return text.lower().startswith(("http://", "https://"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_version_from_url(url: str) -> str | None:
|
|
33
|
+
"""Scan URL path segments for a semver-like segment (e.g. 'v1.8.0').
|
|
34
|
+
|
|
35
|
+
Returns the first matching segment, or None if none is found.
|
|
36
|
+
"""
|
|
37
|
+
path = urlparse(url).path
|
|
38
|
+
for segment in path.split("/"):
|
|
39
|
+
if _SEMVER_RE.match(segment):
|
|
40
|
+
return segment
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _utc_now_iso() -> str:
|
|
45
|
+
"""Return the current UTC time in ISO 8601 format, e.g. '2026-04-06T12:51:28Z'."""
|
|
46
|
+
now = datetime.now(tz=UTC)
|
|
47
|
+
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download_version_from_url(url: str) -> Path:
|
|
51
|
+
"""Download tla2tools.jar from *url* and store it in a timestamped version directory.
|
|
52
|
+
|
|
53
|
+
The version name is extracted from URL path segments. The tag (directory suffix) is
|
|
54
|
+
the ISO 8601 download timestamp.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: if no semver segment can be found in the URL.
|
|
58
|
+
"""
|
|
59
|
+
version_name = extract_version_from_url(url)
|
|
60
|
+
if version_name is None:
|
|
61
|
+
msg = 'could not extract a version name from the URL. The URL must contain a version segment (e.g. "v1.8.0").'
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
|
|
64
|
+
tag = _utc_now_iso()
|
|
65
|
+
tools_dir = get_tools_dir()
|
|
66
|
+
version_dir = tools_dir / f"{version_name}-{tag}"
|
|
67
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
jar_path = version_dir / "tla2tools.jar"
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
response = requests.get(
|
|
72
|
+
url,
|
|
73
|
+
stream=True,
|
|
74
|
+
timeout=30,
|
|
75
|
+
headers={"User-Agent": "tlaplus-cli"},
|
|
76
|
+
)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
total = int(response.headers.get("content-length", 0))
|
|
79
|
+
|
|
80
|
+
with Progress(
|
|
81
|
+
"[progress.description]{task.description}",
|
|
82
|
+
BarColumn(),
|
|
83
|
+
DownloadColumn(),
|
|
84
|
+
TransferSpeedColumn(),
|
|
85
|
+
) as progress:
|
|
86
|
+
task = progress.add_task(f"Downloading {version_name}...", total=total or None)
|
|
87
|
+
with jar_path.open("wb") as f:
|
|
88
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
89
|
+
f.write(chunk)
|
|
90
|
+
progress.update(task, advance=len(chunk))
|
|
91
|
+
except Exception:
|
|
92
|
+
shutil.rmtree(version_dir, ignore_errors=True)
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
write_version_metadata_from_url(version_dir, version_name=version_name, tag=tag, url=url)
|
|
96
|
+
return version_dir
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_version_metadata_from_url(
|
|
100
|
+
version_dir: Path,
|
|
101
|
+
*,
|
|
102
|
+
version_name: str,
|
|
103
|
+
tag: str,
|
|
104
|
+
url: str,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Write meta-tla2tools.json for a URL-sourced install."""
|
|
107
|
+
tlc2_version_string = ""
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
["java", "-cp", "tla2tools.jar", "tlc2.TLC", "-version"],
|
|
111
|
+
cwd=version_dir,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
if result.stdout:
|
|
117
|
+
tlc2_version_string = result.stdout.strip().split("\n")[0]
|
|
118
|
+
except Exception as e:
|
|
119
|
+
typer.echo(f"Warning: Failed to extract TLC version string: {e}", err=True)
|
|
120
|
+
|
|
121
|
+
meta_file = version_dir / "meta-tla2tools.json"
|
|
122
|
+
metadata = {
|
|
123
|
+
"tag_name": version_name,
|
|
124
|
+
"sha": "",
|
|
125
|
+
"published_at": "",
|
|
126
|
+
"tlc2_version_string": tlc2_version_string,
|
|
127
|
+
"prerelease": False,
|
|
128
|
+
"download_url": url,
|
|
129
|
+
"tag": tag,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
with meta_file.open("w") as f:
|
|
134
|
+
json.dump(metadata, f, indent=2)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
typer.echo(f"Warning: Failed to write metadata: {e}", err=True)
|
|
137
|
+
|
|
138
|
+
|
|
25
139
|
def _parse_semver(name: str) -> tuple[int, int, int] | None:
|
|
26
140
|
"""Try to extract (major, minor, patch) from a version name like 'v1.8.0'."""
|
|
27
141
|
m = _SEMVER_RE.match(name)
|
|
@@ -173,7 +287,8 @@ def list_local_versions() -> list[LocalVersion]:
|
|
|
173
287
|
result = []
|
|
174
288
|
for d in tools_dir.iterdir():
|
|
175
289
|
if d.is_dir() and not d.is_symlink():
|
|
176
|
-
|
|
290
|
+
# Split on the FIRST hyphen so timestamp suffixes are preserved whole
|
|
291
|
+
parts = d.name.split("-", 1)
|
|
177
292
|
if len(parts) == 2:
|
|
178
293
|
result.append(LocalVersion(name=parts[0], short_sha=parts[1], path=d))
|
|
179
294
|
return result
|
|
@@ -301,7 +416,8 @@ def _fetch_from_api(
|
|
|
301
416
|
releases_response = requests.get(releases_url, params=params, timeout=10)
|
|
302
417
|
releases_response.raise_for_status()
|
|
303
418
|
releases_data: list[dict[str, Any]] = releases_response.json()
|
|
304
|
-
except
|
|
419
|
+
except Exception as e:
|
|
420
|
+
typer.echo(f"⚠ Warning: Failed to fetch remote versions: {e}", err=True)
|
|
305
421
|
return None
|
|
306
422
|
else:
|
|
307
423
|
return tags_data, releases_data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tlaplus-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: TLA+ tools: download TLC, compile custom modules, run model checker
|
|
5
5
|
Author-email: Denis Nikolskiy <codeomatics@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -170,6 +170,14 @@ Compile modules in a specific project path:
|
|
|
170
170
|
tla modules build path/to/project
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
Configure a persistent custom modules path:
|
|
174
|
+
```bash
|
|
175
|
+
tla modules path /path/to/custom/modules
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> [!TIP]
|
|
179
|
+
> Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
|
|
180
|
+
|
|
173
181
|
Verbose output:
|
|
174
182
|
```bash
|
|
175
183
|
tla modules build --verbose
|
|
@@ -221,6 +229,8 @@ tlc:
|
|
|
221
229
|
java_class: tlc2.TLC
|
|
222
230
|
overrides_class: tlc2.overrides.TLCOverrides
|
|
223
231
|
|
|
232
|
+
module_path: null # (Optional) Persistent custom modules path
|
|
233
|
+
|
|
224
234
|
java:
|
|
225
235
|
min_version: 11
|
|
226
236
|
opts:
|
|
@@ -21,12 +21,9 @@ src/tlaplus_cli.egg-info/requires.txt
|
|
|
21
21
|
src/tlaplus_cli.egg-info/top_level.txt
|
|
22
22
|
src/tlaplus_cli/resources/__init__.py
|
|
23
23
|
src/tlaplus_cli/resources/default_config.yaml
|
|
24
|
-
tests/
|
|
25
|
-
tests/test_check_java.py
|
|
24
|
+
tests/test_build_modules_path.py
|
|
26
25
|
tests/test_cli.py
|
|
27
|
-
tests/
|
|
28
|
-
tests/
|
|
29
|
-
tests/
|
|
30
|
-
tests/
|
|
31
|
-
tests/test_tools_manager.py
|
|
32
|
-
tests/test_version_manager.py
|
|
26
|
+
tests/test_config.py
|
|
27
|
+
tests/test_modules_path.py
|
|
28
|
+
tests/test_settings.py
|
|
29
|
+
tests/test_tlc_modules_path.py
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from tlaplus_cli.cli import app
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_build_uses_configured_module_path(mocker, tmp_path, base_settings, runner):
|
|
5
|
+
"""Test that build uses module_path from config if available."""
|
|
6
|
+
# Custom module path
|
|
7
|
+
custom_modules = tmp_path / "custom_modules"
|
|
8
|
+
custom_modules.mkdir()
|
|
9
|
+
(custom_modules / "MyModule.java").write_text("class MyModule {}")
|
|
10
|
+
|
|
11
|
+
# Update settings
|
|
12
|
+
settings = base_settings.model_copy(deep=True)
|
|
13
|
+
settings.module_path = str(custom_modules)
|
|
14
|
+
mocker.patch("tlaplus_cli.build_tlc_module.load_config", return_value=settings)
|
|
15
|
+
|
|
16
|
+
# Mock subprocess.run
|
|
17
|
+
mock_run = mocker.patch("tlaplus_cli.build_tlc_module.subprocess.run")
|
|
18
|
+
mock_run.return_value.returncode = 0
|
|
19
|
+
|
|
20
|
+
# Mock jar path
|
|
21
|
+
pinned_dir = tmp_path / "tools" / "v1.8.0"
|
|
22
|
+
pinned_dir.mkdir(parents=True)
|
|
23
|
+
(pinned_dir / "tla2tools.jar").write_bytes(b"fake")
|
|
24
|
+
mocker.patch("tlaplus_cli.build_tlc_module.get_pinned_version_dir", return_value=pinned_dir)
|
|
25
|
+
|
|
26
|
+
result = runner.invoke(app, ["modules", "build"])
|
|
27
|
+
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
args, _ = mock_run.call_args
|
|
30
|
+
cmd = args[0]
|
|
31
|
+
# Source file should be from custom_modules
|
|
32
|
+
assert str(custom_modules / "MyModule.java") in cmd
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_build_fails_if_module_path_missing(mocker, tmp_path, base_settings, runner):
|
|
36
|
+
"""Test that build fails if the configured module_path does not exist."""
|
|
37
|
+
settings = base_settings.model_copy(deep=True)
|
|
38
|
+
settings.module_path = str(tmp_path / "non_existent")
|
|
39
|
+
mocker.patch("tlaplus_cli.build_tlc_module.load_config", return_value=settings)
|
|
40
|
+
|
|
41
|
+
# Mock jar path
|
|
42
|
+
pinned_dir = tmp_path / "tools" / "v1.8.0"
|
|
43
|
+
pinned_dir.mkdir(parents=True)
|
|
44
|
+
(pinned_dir / "tla2tools.jar").write_bytes(b"fake")
|
|
45
|
+
mocker.patch("tlaplus_cli.build_tlc_module.get_pinned_version_dir", return_value=pinned_dir)
|
|
46
|
+
|
|
47
|
+
result = runner.invoke(app, ["modules", "build"])
|
|
48
|
+
|
|
49
|
+
assert result.exit_code == 1
|
|
50
|
+
assert "Error: modules directory not found" in result.output
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
import typer
|
|
3
|
-
from typer.testing import CliRunner
|
|
4
3
|
|
|
5
4
|
from tlaplus_cli.cli import app, version_callback
|
|
6
5
|
|
|
7
|
-
runner = CliRunner()
|
|
8
|
-
|
|
9
6
|
|
|
10
7
|
def test_version_callback_exits(mocker):
|
|
11
8
|
"""Test version callback exits after printing."""
|
|
@@ -22,7 +19,7 @@ def test_version_callback_exits(mocker):
|
|
|
22
19
|
mock_metadata.assert_called_once_with("tlaplus-cli")
|
|
23
20
|
|
|
24
21
|
|
|
25
|
-
def test_cli_version_flag(mocker):
|
|
22
|
+
def test_cli_version_flag(mocker, runner):
|
|
26
23
|
"""Test 'tla --version' command."""
|
|
27
24
|
mock_metadata = mocker.patch("importlib.metadata.metadata")
|
|
28
25
|
mock_metadata.return_value = {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from tlaplus_cli import config
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_config_paths(mocker, tmp_path):
|
|
5
|
+
"""Test config_dir, config_path, cache_dir return correct paths."""
|
|
6
|
+
mocker.patch("platformdirs.user_config_dir", return_value=str(tmp_path / "config"))
|
|
7
|
+
mocker.patch("platformdirs.user_cache_dir", return_value=str(tmp_path / "cache"))
|
|
8
|
+
|
|
9
|
+
assert config.config_dir() == tmp_path / "config"
|
|
10
|
+
assert config.config_path() == tmp_path / "config" / "config.yaml"
|
|
11
|
+
assert config.cache_dir() == tmp_path / "cache"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_ensure_config_creates_default(mocker, tmp_path):
|
|
15
|
+
"""_ensure_config creates the config file if it doesn't exist."""
|
|
16
|
+
config_dir = tmp_path / "config"
|
|
17
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
18
|
+
config_path = config_dir / "config.yaml"
|
|
19
|
+
|
|
20
|
+
assert not config_path.exists()
|
|
21
|
+
path = config._ensure_config()
|
|
22
|
+
assert path == config_path
|
|
23
|
+
assert config_path.exists()
|
|
24
|
+
assert "workspace:" in config_path.read_text()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_load_config_caching(mocker, tmp_path):
|
|
28
|
+
"""load_config should be cached."""
|
|
29
|
+
config.load_config.cache_clear()
|
|
30
|
+
|
|
31
|
+
config_dir = tmp_path / "config"
|
|
32
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
33
|
+
config._ensure_config()
|
|
34
|
+
|
|
35
|
+
# First call
|
|
36
|
+
c1 = config.load_config()
|
|
37
|
+
# Second call should return the same object from cache
|
|
38
|
+
c2 = config.load_config()
|
|
39
|
+
assert c1 is c2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_workspace_root_absolute(mocker, tmp_path):
|
|
43
|
+
"""workspace_root returns absolute path when config has absolute path."""
|
|
44
|
+
abs_path = (tmp_path / "my_ws").absolute()
|
|
45
|
+
mock_settings = mocker.MagicMock()
|
|
46
|
+
mock_settings.workspace.root = str(abs_path)
|
|
47
|
+
mocker.patch("tlaplus_cli.config.load_config", return_value=mock_settings)
|
|
48
|
+
|
|
49
|
+
assert config.workspace_root() == abs_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_load_module_path(mocker, tmp_path):
|
|
53
|
+
"""Test that module_path can be loaded from config."""
|
|
54
|
+
config.load_config.cache_clear()
|
|
55
|
+
config_dir = tmp_path / "config"
|
|
56
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
57
|
+
config_path = config_dir / "config.yaml"
|
|
58
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
config_content = """
|
|
61
|
+
tla:
|
|
62
|
+
urls:
|
|
63
|
+
tags: https://example.com/tags
|
|
64
|
+
releases: https://example.com/releases
|
|
65
|
+
per_page: 30
|
|
66
|
+
workspace:
|
|
67
|
+
root: .
|
|
68
|
+
spec_dir: spec
|
|
69
|
+
modules_dir: modules
|
|
70
|
+
classes_dir: classes
|
|
71
|
+
tlc:
|
|
72
|
+
java_class: tlc2.TLC
|
|
73
|
+
overrides_class: tlc2.overrides.TLCOverrides
|
|
74
|
+
module_path: /custom/path
|
|
75
|
+
"""
|
|
76
|
+
config_path.write_text(config_content)
|
|
77
|
+
|
|
78
|
+
cfg = config.load_config()
|
|
79
|
+
assert cfg.module_path == "/custom/path"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_save_config(mocker, tmp_path):
|
|
83
|
+
"""Test saving config preserves module_path and uses deep copy."""
|
|
84
|
+
config.load_config.cache_clear()
|
|
85
|
+
config_dir = tmp_path / "config"
|
|
86
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
87
|
+
config_path = config_dir / "config.yaml"
|
|
88
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
cfg = config.load_config()
|
|
91
|
+
cfg.module_path = "/new/path"
|
|
92
|
+
|
|
93
|
+
config.save_config(cfg)
|
|
94
|
+
|
|
95
|
+
# Reload and check
|
|
96
|
+
config.load_config.cache_clear()
|
|
97
|
+
new_cfg = config.load_config()
|
|
98
|
+
assert new_cfg.module_path == "/new/path"
|
|
99
|
+
assert "module_path: /new/path" in config_path.read_text()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from tlaplus_cli import config
|
|
2
|
+
from tlaplus_cli.cli import app
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_modules_path_success(mocker, tmp_path, runner):
|
|
6
|
+
"""Test successful setting of modules path."""
|
|
7
|
+
config.load_config.cache_clear()
|
|
8
|
+
config_dir = tmp_path / "config"
|
|
9
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
10
|
+
|
|
11
|
+
# Create a dummy module directory
|
|
12
|
+
module_dir = tmp_path / "my_modules"
|
|
13
|
+
module_dir.mkdir()
|
|
14
|
+
|
|
15
|
+
result = runner.invoke(app, ["modules", "path", str(module_dir)])
|
|
16
|
+
|
|
17
|
+
assert result.exit_code == 0
|
|
18
|
+
assert f"Modules path updated to: {module_dir}" in result.output
|
|
19
|
+
|
|
20
|
+
# Check config
|
|
21
|
+
config.load_config.cache_clear()
|
|
22
|
+
cfg = config.load_config()
|
|
23
|
+
assert cfg.module_path == str(module_dir)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_modules_path_invalid_dir(mocker, tmp_path, runner):
|
|
27
|
+
"""Test setting modules path to non-existent directory fails."""
|
|
28
|
+
config.load_config.cache_clear()
|
|
29
|
+
config_dir = tmp_path / "config"
|
|
30
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
31
|
+
|
|
32
|
+
invalid_dir = tmp_path / "does_not_exist"
|
|
33
|
+
|
|
34
|
+
result = runner.invoke(app, ["modules", "path", str(invalid_dir)])
|
|
35
|
+
|
|
36
|
+
assert result.exit_code == 1
|
|
37
|
+
assert "Error: Path does not exist or is not a directory" in result.output
|
|
38
|
+
|
|
39
|
+
# Check config was NOT updated
|
|
40
|
+
config.load_config.cache_clear()
|
|
41
|
+
cfg = config.load_config()
|
|
42
|
+
assert cfg.module_path is None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_modules_path_file_instead_of_dir(mocker, tmp_path, runner):
|
|
46
|
+
"""Test setting modules path to a file fails."""
|
|
47
|
+
config.load_config.cache_clear()
|
|
48
|
+
config_dir = tmp_path / "config"
|
|
49
|
+
mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
|
|
50
|
+
|
|
51
|
+
some_file = tmp_path / "some_file.txt"
|
|
52
|
+
some_file.write_text("hello")
|
|
53
|
+
|
|
54
|
+
result = runner.invoke(app, ["modules", "path", str(some_file)])
|
|
55
|
+
|
|
56
|
+
assert result.exit_code == 1
|
|
57
|
+
assert "Error: Path does not exist or is not a directory" in result.output
|
|
58
|
+
|
|
59
|
+
# Check config was NOT updated
|
|
60
|
+
config.load_config.cache_clear()
|
|
61
|
+
cfg = config.load_config()
|
|
62
|
+
assert cfg.module_path is None
|