tlaplus-cli 0.1.7__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Denis Nikolskiy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include tla/resources *.yaml
4
+ include tla/py.typed
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: tlaplus-cli
3
+ Version: 0.1.7
4
+ Summary: TLA+ tools: download TLC, compile custom modules, run model checker
5
+ Author-email: Denis Nikolskiy <codeomatics@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nikolskiy/tlaplus-cli
8
+ Project-URL: Repository, https://github.com/nikolskiy/tlaplus-cli
9
+ Project-URL: Issues, https://github.com/nikolskiy/tlaplus-cli/issues
10
+ Project-URL: Changelog, https://github.com/nikolskiy/tlaplus-cli/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: typer<1.0.0,>=0.22.0
23
+ Requires-Dist: requests<3.0.0,>=2.31.0
24
+ Requires-Dist: pyyaml<7.0,>=6.0
25
+ Requires-Dist: platformdirs<5.0,>=4.0
26
+ Requires-Dist: pydantic<3.0.0,>=2.12.5
27
+ Dynamic: license-file
28
+
29
+ # TLA+ CLI
30
+
31
+ Command-line tool for working with TLA+ specifications and the TLC model checker.
32
+
33
+ ## Installation
34
+
35
+ Install system-wide via uv tool
36
+
37
+ ```bash
38
+ uv tool install git+https://github.com/nikolskiy/tlaplus-cli
39
+ ```
40
+
41
+ Upgrade:
42
+ ```bash
43
+ uv tool upgrade tlaplus-cli
44
+ ```
45
+
46
+ Uninstall
47
+
48
+ ```bash
49
+ uv tool uninstall tlaplus-cli
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Download TLC
55
+
56
+ ```bash
57
+ # Download stable release
58
+ tla download
59
+
60
+ # Download nightly build
61
+ tla download --nightly
62
+ ```
63
+
64
+ ### Check Java Version
65
+
66
+ ```bash
67
+ tla check-java
68
+ ```
69
+
70
+ ### Compile Custom Java Modules
71
+
72
+ ```bash
73
+ tla build
74
+
75
+ # Verbose output
76
+ tla build --verbose
77
+ ```
78
+
79
+ Compiles `.java` files from `workspace/modules/` into `workspace/classes/`.
80
+
81
+ ### Run TLC
82
+
83
+ ```bash
84
+ tla tlc <spec_name>
85
+ ```
86
+
87
+ For example:
88
+
89
+ ```bash
90
+ tla tlc queue
91
+ ```
92
+
93
+ ## Configuration
94
+
95
+ On first run, a default config is created at:
96
+
97
+ ```
98
+ ~/.config/tla/config.yaml
99
+ ```
100
+
101
+ Edit this file to set your workspace path and TLC options:
102
+
103
+ ```yaml
104
+ tla:
105
+ jar_name: tla2tools.jar
106
+ urls:
107
+ stable: https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar
108
+ nightly: https://tla.msr-inria.inria.fr/tlatoolbox/ci/dist/tla2tools.jar
109
+
110
+ workspace:
111
+ root: . # Project root (relative to CWD)
112
+ spec_dir: spec # Directory containing .tla files
113
+ modules_dir: modules # Directory containing .java files
114
+ classes_dir: classes # Directory for compiled .class files
115
+
116
+ tlc:
117
+ java_class: tlc2.TLC
118
+ overrides_class: tlc2.overrides.TLCOverrides
119
+
120
+ java:
121
+ min_version: 11
122
+ opts:
123
+ - "-XX:+IgnoreUnrecognizedVMOptions"
124
+ - "-XX:+UseParallelGC"
125
+ ```
126
+
127
+ ### Directory Layout
128
+
129
+ | Directory | Purpose | Location |
130
+ |---|---|---|
131
+ | Config | `config.yaml` | `~/.config/tla/` |
132
+ | Cache | `tla2tools.jar` | `~/.cache/tla/` |
133
+ | Workspace | specs + modules + classes | Set via `workspace.root` in config |
134
+
135
+ ## Note on Package Name
136
+
137
+ This package is distributed on PyPI as **`tlaplus-cli`** but imports as **`tla`**. There is a separate, unrelated [`tla`](https://pypi.org/project/tla/) package on PyPI (a TLA+ parser). If you have both installed, they will conflict. In practice this is unlikely since they serve different purposes, but be aware of it.
138
+
139
+ ## Dependencies
140
+
141
+ * **Java >= 11**: Required for TLC.
142
+ * [**uv**](https://docs.astral.sh/uv/getting-started/installation/): For installing the tool.
@@ -0,0 +1,114 @@
1
+ # TLA+ CLI
2
+
3
+ Command-line tool for working with TLA+ specifications and the TLC model checker.
4
+
5
+ ## Installation
6
+
7
+ Install system-wide via uv tool
8
+
9
+ ```bash
10
+ uv tool install git+https://github.com/nikolskiy/tlaplus-cli
11
+ ```
12
+
13
+ Upgrade:
14
+ ```bash
15
+ uv tool upgrade tlaplus-cli
16
+ ```
17
+
18
+ Uninstall
19
+
20
+ ```bash
21
+ uv tool uninstall tlaplus-cli
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Download TLC
27
+
28
+ ```bash
29
+ # Download stable release
30
+ tla download
31
+
32
+ # Download nightly build
33
+ tla download --nightly
34
+ ```
35
+
36
+ ### Check Java Version
37
+
38
+ ```bash
39
+ tla check-java
40
+ ```
41
+
42
+ ### Compile Custom Java Modules
43
+
44
+ ```bash
45
+ tla build
46
+
47
+ # Verbose output
48
+ tla build --verbose
49
+ ```
50
+
51
+ Compiles `.java` files from `workspace/modules/` into `workspace/classes/`.
52
+
53
+ ### Run TLC
54
+
55
+ ```bash
56
+ tla tlc <spec_name>
57
+ ```
58
+
59
+ For example:
60
+
61
+ ```bash
62
+ tla tlc queue
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ On first run, a default config is created at:
68
+
69
+ ```
70
+ ~/.config/tla/config.yaml
71
+ ```
72
+
73
+ Edit this file to set your workspace path and TLC options:
74
+
75
+ ```yaml
76
+ tla:
77
+ jar_name: tla2tools.jar
78
+ urls:
79
+ stable: https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar
80
+ nightly: https://tla.msr-inria.inria.fr/tlatoolbox/ci/dist/tla2tools.jar
81
+
82
+ workspace:
83
+ root: . # Project root (relative to CWD)
84
+ spec_dir: spec # Directory containing .tla files
85
+ modules_dir: modules # Directory containing .java files
86
+ classes_dir: classes # Directory for compiled .class files
87
+
88
+ tlc:
89
+ java_class: tlc2.TLC
90
+ overrides_class: tlc2.overrides.TLCOverrides
91
+
92
+ java:
93
+ min_version: 11
94
+ opts:
95
+ - "-XX:+IgnoreUnrecognizedVMOptions"
96
+ - "-XX:+UseParallelGC"
97
+ ```
98
+
99
+ ### Directory Layout
100
+
101
+ | Directory | Purpose | Location |
102
+ |---|---|---|
103
+ | Config | `config.yaml` | `~/.config/tla/` |
104
+ | Cache | `tla2tools.jar` | `~/.cache/tla/` |
105
+ | Workspace | specs + modules + classes | Set via `workspace.root` in config |
106
+
107
+ ## Note on Package Name
108
+
109
+ This package is distributed on PyPI as **`tlaplus-cli`** but imports as **`tla`**. There is a separate, unrelated [`tla`](https://pypi.org/project/tla/) package on PyPI (a TLA+ parser). If you have both installed, they will conflict. In practice this is unlikely since they serve different purposes, but be aware of it.
110
+
111
+ ## Dependencies
112
+
113
+ * **Java >= 11**: Required for TLC.
114
+ * [**uv**](https://docs.astral.sh/uv/getting-started/installation/): For installing the tool.
@@ -0,0 +1,137 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tlaplus-cli"
7
+ version = "0.1.7"
8
+ description = "TLA+ tools: download TLC, compile custom modules, run model checker"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "typer>=0.22.0,<1.0.0",
13
+ "requests>=2.31.0,<3.0.0",
14
+ "pyyaml>=6.0,<7.0",
15
+ "platformdirs>=4.0,<5.0",
16
+ "pydantic>=2.12.5,<3.0.0",
17
+ ]
18
+ authors = [
19
+ {name = "Denis Nikolskiy", email = "codeomatics@gmail.com"}
20
+ ]
21
+ license = "MIT"
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Operating System :: OS Independent",
29
+ "Topic :: Software Development :: Build Tools",
30
+ "Topic :: Utilities",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/nikolskiy/tlaplus-cli"
35
+ Repository = "https://github.com/nikolskiy/tlaplus-cli"
36
+ Issues = "https://github.com/nikolskiy/tlaplus-cli/issues"
37
+ Changelog = "https://github.com/nikolskiy/tlaplus-cli/blob/main/CHANGELOG.md"
38
+
39
+ [project.scripts]
40
+ tla = "tla.cli:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["tla*"]
44
+
45
+ [tool.setuptools.package-data]
46
+ tla = ["resources/default_config.yaml", "py.typed"]
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "pytest>=9.0.2,<10.0.0",
51
+ "pytest-cov>=7.0.0,<8.0.0",
52
+ "pytest-mock>=3.15.1,<4.0.0",
53
+ "ruff>=0.9.0,<1.0.0",
54
+ "mypy>=1.0.0,<2.0.0",
55
+ "types-PyYAML>=6.0.12.12,<7.0.0.0",
56
+ "types-requests>=2.31.0.2,<3.0.0.0",
57
+ "poethepoet>=0.24.0,<1.0.0",
58
+ "detect-secrets>=1.5.0,<2.0.0",
59
+ "pre-commit>=3.5.0,<4.0.0",
60
+ "pre-commit-hooks>=6.0.0,<7.0.0",
61
+ "twine>=6.0.0,<7.0.0",
62
+ ]
63
+
64
+ [tool.pytest.ini_options]
65
+ addopts = "--cov=tla --cov-report=term-missing"
66
+ testpaths = ["tests"]
67
+
68
+ [tool.ruff]
69
+ line-length = 120
70
+ target-version = "py311"
71
+
72
+ [tool.ruff.lint]
73
+ select = [
74
+ "F", # Pyflakes: logic errors
75
+ "E", # pycodestyle errors
76
+ "W", # pycodestyle warnings
77
+ "I", # isort: import sorting
78
+ "N", # pep8-naming: naming conventions
79
+ "UP", # pyupgrade: upgrade syntax for newer python versions
80
+ "PL", # Pylint: various checks (convention, refactor, warning, etc.)
81
+ "B", # flake8-bugbear: finds likely bugs and design problems
82
+ "SIM", # flake8-simplify: suggests simpler python constructs
83
+ "C4", # flake8-comprehensions: better list/set/dict comprehensions
84
+ "PIE", # flake8-pie: miscellaneous lints
85
+ "TCH", # flake8-type-checking: move imports to type-checking blocks
86
+ "PGH", # pygrep-hooks: regex based checks
87
+ "G", # flake8-logging-format: validate logging format strings
88
+ "RUF", # Ruff-specific rules
89
+ "PTH", # flake8-use-pathlib: replace os.path with pathlib
90
+ "ARG", # flake8-unused-arguments: find unused function arguments
91
+ "RET", # flake8-return: cleaner return logic
92
+ "TRY", # tryceratops: prevent exception handling anti-patterns
93
+ ]
94
+ ignore = [
95
+ "PLR2004", # Magic value used in comparison
96
+ "PLW1510", # subprocess.run without explicit check
97
+ ]
98
+
99
+ [tool.ruff.lint.per-file-ignores]
100
+ "tests/*" = [
101
+ "ARG001", # Unused arguments are common in tests (fixtures)
102
+ ]
103
+
104
+ [tool.poe.tasks]
105
+ check = "pre-commit run --all-files"
106
+ lint = ["lint-ruff", "type-check"]
107
+ lint-ruff = "ruff check ."
108
+ type-check = "mypy ."
109
+ fix = ["format", "lint-fix"]
110
+ format = "ruff format ."
111
+ lint-fix = "ruff check --fix ."
112
+ test = "pytest"
113
+ install-hooks = "pre-commit install --install-hooks"
114
+ audit = "detect-secrets audit .secrets.baseline"
115
+ build = "uv build"
116
+ clean = "rm -rf dist"
117
+
118
+ twine-check = "twine check dist/*.whl dist/*.tar.gz"
119
+
120
+ [tool.poe.tasks.check-release]
121
+ sequence = [
122
+ "check",
123
+ "clean",
124
+ "build",
125
+ "twine-check",
126
+ ]
127
+
128
+ [tool.mypy]
129
+ python_version = "3.11"
130
+ strict = true
131
+ ignore_missing_imports = false
132
+ exclude = ["build", "tests/"]
133
+
134
+ # Per-module overrides for libraries without strict types
135
+ [[tool.mypy.overrides]]
136
+ module = "platformdirs.*"
137
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,81 @@
1
+ import shutil
2
+
3
+ import pytest
4
+
5
+ from tla import build_tlc_module
6
+
7
+ # Define paths relative to the test file
8
+ # Check if javac is available
9
+ JAVAC_AVAILABLE = shutil.which("javac") is not None
10
+
11
+
12
+ @pytest.mark.skipif(not JAVAC_AVAILABLE, reason="javac not found")
13
+ def test_build_integration(mocker, tmp_path, queue_dir, base_settings):
14
+ """
15
+ Integration test for build_tlc_module.build().
16
+ Uses the actual files in tla-example and the real tla2tools.jar (if available).
17
+ Verified output classes are generated in a temporary directory.
18
+ """
19
+ # Create a temporary classes directory
20
+ classes_dir = tmp_path / "classes"
21
+
22
+ # Configure base_settings for this test
23
+ base_settings.workspace.root = queue_dir
24
+ base_settings.workspace.classes_dir = classes_dir
25
+
26
+ # Patch load_config to return our mock config
27
+ mocker.patch("tla.build_tlc_module.load_config", return_value=base_settings)
28
+
29
+ # Patch workspace_root to return the example directory
30
+ mocker.patch("tla.build_tlc_module.workspace_root", return_value=queue_dir)
31
+
32
+ # Run build
33
+ # We catch typer.Exit just in case, though it shouldn't be raised on success
34
+ try:
35
+ build_tlc_module.build(verbose=True)
36
+ except SystemExit as e:
37
+ # build() raises typer.Exit(1) on failure
38
+ # If it raises, we fail the test
39
+ assert e.code == 0, "Build failed with SystemExit"
40
+
41
+ # Verify outputs
42
+ assert classes_dir.exists(), "Classes directory not created"
43
+
44
+ # Verify TLCOverrides.class exists
45
+ # The source is modules/tlc2/overrides/TLCOverrides.java
46
+ # Package is tlc2.overrides, so output should be in tlc2/overrides/
47
+ output_class = classes_dir / "tlc2/overrides/TLCOverrides.class"
48
+ assert output_class.exists(), f"Class file not found at {output_class}"
49
+
50
+ # Verify META-INF service file
51
+ service_file = classes_dir / "META-INF/services/tlc2.overrides.ITLCOverrides"
52
+ assert service_file.exists(), "Service file not found"
53
+
54
+ content = service_file.read_text().strip()
55
+ assert content == "tlc2.overrides.TLCOverrides", f"Unexpected service file content: {content}"
56
+
57
+
58
+ @pytest.mark.skipif(not JAVAC_AVAILABLE, reason="javac not found")
59
+ def test_build_custom_overrides(mocker, tmp_path, queue_dir, base_settings):
60
+ """
61
+ Test build with custom overrides class configuration.
62
+ """
63
+ classes_dir = tmp_path / "classes"
64
+
65
+ # Configure custom override
66
+ custom_override = "com.example.MyOverrides"
67
+ base_settings.tlc.overrides_class = custom_override
68
+ base_settings.workspace.root = queue_dir
69
+ base_settings.workspace.classes_dir = classes_dir
70
+
71
+ mocker.patch("tla.build_tlc_module.load_config", return_value=base_settings)
72
+ mocker.patch("tla.build_tlc_module.workspace_root", return_value=queue_dir)
73
+
74
+ try:
75
+ build_tlc_module.build(verbose=False)
76
+ except SystemExit as e:
77
+ assert e.code == 0
78
+
79
+ service_file = classes_dir / "META-INF/services/tlc2.overrides.ITLCOverrides"
80
+ assert service_file.exists()
81
+ assert service_file.read_text().strip() == custom_override
@@ -0,0 +1,59 @@
1
+ from unittest.mock import MagicMock
2
+
3
+ import pytest
4
+ import typer
5
+
6
+ from tla import check_java
7
+
8
+
9
+ def test_parse_java_version():
10
+ """Test parsing of Java version strings."""
11
+ assert check_java.parse_java_version("1.8.0_202") == 8
12
+ assert check_java.parse_java_version("9.0.4") == 9
13
+ assert check_java.parse_java_version("11.0.2") == 11
14
+ assert check_java.parse_java_version("17") == 17
15
+ assert check_java.parse_java_version("21.0.1") == 21
16
+
17
+
18
+ def test_get_java_version_success(mocker):
19
+ """Test successful retrieval of Java version."""
20
+ mock_run = mocker.patch("subprocess.run")
21
+ # Simulate java -version output (it usually goes to stderr)
22
+ mock_run.return_value = MagicMock(
23
+ stdout="", stderr='openjdk version "11.0.2" 2019-01-15\nOpenJDK Runtime Environment 18.9...'
24
+ )
25
+ mocker.patch("shutil.which", return_value="/usr/bin/java")
26
+
27
+ assert check_java.get_java_version() == "11.0.2"
28
+
29
+
30
+ def test_get_java_version_not_found(mocker):
31
+ """Test when java is not found in PATH."""
32
+ mocker.patch("shutil.which", return_value=None)
33
+ assert check_java.get_java_version() is None
34
+
35
+
36
+ def test_check_java_version_ok(mocker):
37
+ """Test check passes when version is sufficient."""
38
+ mocker.patch("tla.check_java.get_java_version", return_value="11.0.2")
39
+ # Should not raise
40
+ check_java.check_java_version(11)
41
+ check_java.check_java_version(8)
42
+
43
+
44
+ def test_check_java_version_too_low(mocker):
45
+ """Test check fails when version is too low."""
46
+ mocker.patch("tla.check_java.get_java_version", return_value="1.8.0_202")
47
+
48
+ with pytest.raises(typer.Exit) as e:
49
+ check_java.check_java_version(11)
50
+ assert e.value.exit_code == 1
51
+
52
+
53
+ def test_check_java_version_missing(mocker):
54
+ """Test check fails when java is missing."""
55
+ mocker.patch("tla.check_java.get_java_version", return_value=None)
56
+
57
+ with pytest.raises(typer.Exit) as e:
58
+ check_java.check_java_version(11)
59
+ assert e.value.exit_code == 1
@@ -0,0 +1,43 @@
1
+
2
+ import pytest
3
+ import typer
4
+ from typer.testing import CliRunner
5
+
6
+ from tla.cli import app, version_callback
7
+
8
+ runner = CliRunner()
9
+
10
+
11
+ def test_version_callback_exits(mocker):
12
+ """Test version callback exits after printing."""
13
+ mock_metadata = mocker.patch("importlib.metadata.metadata")
14
+ mock_metadata.return_value = {
15
+ "Name": "tlaplus-cli",
16
+ "Version": "1.2.3",
17
+ "Summary": "Test summary",
18
+ }
19
+
20
+ with pytest.raises(typer.Exit):
21
+ version_callback(True)
22
+
23
+ mock_metadata.assert_called_once_with("tlaplus-cli")
24
+
25
+
26
+ def test_cli_version_flag(mocker):
27
+ """Test 'tla --version' command."""
28
+ mock_metadata = mocker.patch("importlib.metadata.metadata")
29
+ mock_metadata.return_value = {
30
+ "Name": "tlaplus-cli",
31
+ "Version": "1.2.3",
32
+ "Summary": "Test summary",
33
+ }
34
+
35
+ # Running the CLI with --version
36
+ result = runner.invoke(app, ["--version"])
37
+
38
+ # It should exit with 0
39
+ assert result.exit_code == 0
40
+ assert "tlaplus-cli v1.2.3" in result.stdout
41
+ assert "Test summary" in result.stdout
42
+
43
+ mock_metadata.assert_called_once_with("tlaplus-cli")