leanup 0.0.2__tar.gz → 0.0.3__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.
- {leanup-0.0.2 → leanup-0.0.3}/PKG-INFO +8 -3
- {leanup-0.0.2 → leanup-0.0.3}/README.md +2 -2
- {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/leanup/__init__.py +3 -1
- leanup-0.0.3/leanup/cli.py +12 -0
- leanup-0.0.3/leanup/const.py +15 -0
- leanup-0.0.3/leanup/utils/__init__.py +1 -0
- leanup-0.0.3/leanup/utils/executor.py +183 -0
- {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/PKG-INFO +8 -3
- leanup-0.0.3/leanup.egg-info/SOURCES.txt +29 -0
- leanup-0.0.3/leanup.egg-info/entry_points.txt +2 -0
- leanup-0.0.3/leanup.egg-info/requires.txt +11 -0
- {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/top_level.txt +0 -1
- {leanup-0.0.2 → leanup-0.0.3}/pyproject.toml +9 -2
- leanup-0.0.3/tests/conftest.py +22 -0
- leanup-0.0.3/tests/test_executor.py +88 -0
- leanup-0.0.3/tests/test_leanup.py +17 -0
- leanup-0.0.2/build/lib/build/lib/cli/__init__.py +0 -15
- leanup-0.0.2/build/lib/build/lib/cli/install.py +0 -143
- leanup-0.0.2/build/lib/build/lib/cli/main.py +0 -184
- leanup-0.0.2/build/lib/build/lib/cli/utils.py +0 -245
- leanup-0.0.2/build/lib/build/lib/dist/leanup-0.0.1-py3-none-any.whl +0 -0
- leanup-0.0.2/build/lib/build/lib/dist/leanup-0.0.1.tar.gz +0 -0
- leanup-0.0.2/build/lib/build/lib/tests/test_leanup.py +0 -10
- leanup-0.0.2/build/lib/cli/__init__.py +0 -15
- leanup-0.0.2/build/lib/cli/install.py +0 -143
- leanup-0.0.2/build/lib/cli/main.py +0 -184
- leanup-0.0.2/build/lib/cli/utils.py +0 -245
- leanup-0.0.2/build/lib/dist/leanup-0.0.1-py3-none-any.whl +0 -0
- leanup-0.0.2/build/lib/dist/leanup-0.0.1.tar.gz +0 -0
- leanup-0.0.2/build/lib/dist/leanup-0.0.2-py3-none-any.whl +0 -0
- leanup-0.0.2/build/lib/dist/leanup-0.0.2.tar.gz +0 -0
- leanup-0.0.2/build/lib/leanup/__init__.py +0 -5
- leanup-0.0.2/build/lib/leanup/leanup.py +0 -1
- leanup-0.0.2/build/lib/tests/__init__.py +0 -1
- leanup-0.0.2/build/lib/tests/test_leanup.py +0 -10
- leanup-0.0.2/dist/leanup-0.0.1-py3-none-any.whl +0 -0
- leanup-0.0.2/dist/leanup-0.0.1.tar.gz +0 -0
- leanup-0.0.2/dist/leanup-0.0.2-py3-none-any.whl +0 -0
- leanup-0.0.2/dist/leanup-0.0.2.tar.gz +0 -0
- leanup-0.0.2/leanup/__init__.py +0 -5
- leanup-0.0.2/leanup/leanup.py +0 -1
- leanup-0.0.2/leanup.egg-info/SOURCES.txt +0 -46
- leanup-0.0.2/leanup.egg-info/requires.txt +0 -6
- leanup-0.0.2/tests/__init__.py +0 -1
- leanup-0.0.2/tests/test_leanup.py +0 -10
- {leanup-0.0.2 → leanup-0.0.3}/LICENSE +0 -0
- {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/leanup/leanup.py +0 -0
- {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/dependency_links.txt +0 -0
- {leanup-0.0.2 → leanup-0.0.3}/setup.cfg +0 -0
- {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: leanup
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Python package for Lean Environment Management
|
|
5
5
|
Author-email: Lean-zh Community <leanprover@outlook.com>
|
|
6
6
|
Maintainer-email: Lean-zh Community <leanprover@outlook.com>
|
|
@@ -10,6 +10,11 @@ Project-URL: changelog, https://github.com/{{ cookiecutter.github_username }}/{{
|
|
|
10
10
|
Project-URL: homepage, https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: gitpython
|
|
14
|
+
Requires-Dist: psutil
|
|
15
|
+
Requires-Dist: click
|
|
16
|
+
Requires-Dist: platformdirs
|
|
17
|
+
Requires-Dist: loguru
|
|
13
18
|
Provides-Extra: dev
|
|
14
19
|
Requires-Dist: coverage; extra == "dev"
|
|
15
20
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -23,8 +28,8 @@ Dynamic: license-file
|
|
|
23
28
|
<a href="https://pypi.python.org/pypi/leanup">
|
|
24
29
|
<img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
|
|
25
30
|
</a>
|
|
26
|
-
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
27
|
-
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
31
|
+
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
|
|
32
|
+
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
|
|
28
33
|
</a>
|
|
29
34
|
<a href="https://codecov.io/gh/Lean-zh/leanup">
|
|
30
35
|
<img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<a href="https://pypi.python.org/pypi/leanup">
|
|
5
5
|
<img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
|
|
6
6
|
</a>
|
|
7
|
-
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
8
|
-
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
7
|
+
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
|
|
8
|
+
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
|
|
9
9
|
</a>
|
|
10
10
|
<a href="https://codecov.io/gh/Lean-zh/leanup">
|
|
11
11
|
<img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import platformdirs
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
OS_TYPE = None
|
|
7
|
+
if platform.system() == 'Windows':
|
|
8
|
+
OS_TYPE = 'Windows'
|
|
9
|
+
elif platform.system() == 'Darwin':
|
|
10
|
+
OS_TYPE = 'MacOS'
|
|
11
|
+
elif platform.system() == 'Linux':
|
|
12
|
+
OS_TYPE = 'Linux'
|
|
13
|
+
|
|
14
|
+
LEANUP_CACHE_DIR = Path(
|
|
15
|
+
os.getenv('LEANUP_CACHE_DIR', platformdirs.user_cache_dir("leanup")))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from leanup.utils.executor import CommandExecutor
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import tempfile
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Optional, Union, Tuple, List, Dict, Any, Generator
|
|
7
|
+
import git
|
|
8
|
+
from psutil import Process, NoSuchProcess
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class CommandExecutor:
|
|
14
|
+
def __init__(self, cwd: Optional[str] = None, timeout: Optional[int] = None):
|
|
15
|
+
"""
|
|
16
|
+
Initialize command executor
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
cwd: Default working directory
|
|
20
|
+
timeout: Default timeout for commands
|
|
21
|
+
"""
|
|
22
|
+
self.cwd = cwd
|
|
23
|
+
self.timeout = timeout
|
|
24
|
+
self.active_processes = [] # Track active processes
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def working_directory(
|
|
28
|
+
self,
|
|
29
|
+
path: Optional[Union[str, Path]] = None,
|
|
30
|
+
chdir: bool = False,
|
|
31
|
+
) -> Generator[Path, None, None]:
|
|
32
|
+
"""
|
|
33
|
+
Context manager for working directory operations
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
path: Target working directory path. If None, creates a temporary directory
|
|
37
|
+
chdir: Whether to actually change the current working directory
|
|
38
|
+
|
|
39
|
+
Yields:
|
|
40
|
+
Path object representing working directory
|
|
41
|
+
"""
|
|
42
|
+
origin = Path.cwd()
|
|
43
|
+
if path is None: # Create temporary directory
|
|
44
|
+
tmp_dir = tempfile.TemporaryDirectory()
|
|
45
|
+
path = tmp_dir.__enter__()
|
|
46
|
+
is_temporary = True
|
|
47
|
+
else:
|
|
48
|
+
is_temporary = False
|
|
49
|
+
|
|
50
|
+
path = Path(path)
|
|
51
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
if chdir: os.chdir(path)
|
|
54
|
+
try:
|
|
55
|
+
yield path
|
|
56
|
+
finally:
|
|
57
|
+
if chdir: os.chdir(origin)
|
|
58
|
+
if is_temporary:
|
|
59
|
+
tmp_dir.__exit__(None, None, None)
|
|
60
|
+
|
|
61
|
+
def execute_in_directory(self, command: list,
|
|
62
|
+
directory: Optional[Union[str, Path]] = None,
|
|
63
|
+
chdir: bool = False,
|
|
64
|
+
**kwargs) -> Tuple[str, str, int]:
|
|
65
|
+
"""
|
|
66
|
+
Execute command in specified directory using context manager
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
command: Command list to execute
|
|
70
|
+
directory: Target directory for execution
|
|
71
|
+
chdir: Whether to change working directory
|
|
72
|
+
**kwargs: Additional arguments passed to execute()
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple containing (stdout, stderr, return_code)
|
|
76
|
+
"""
|
|
77
|
+
with self.working_directory(directory, chdir=chdir) as work_dir:
|
|
78
|
+
return self.execute(command, cwd=str(work_dir), **kwargs)
|
|
79
|
+
|
|
80
|
+
def execute(self, command: list,
|
|
81
|
+
cwd: Optional[str] = None,
|
|
82
|
+
text: bool = True,
|
|
83
|
+
input: Union[str, None] = None,
|
|
84
|
+
capture_output: bool = True,
|
|
85
|
+
timeout: Optional[int] = None) -> Tuple[str, str, int]:
|
|
86
|
+
"""
|
|
87
|
+
Execute command and capture output
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
command: Command list to execute
|
|
91
|
+
cwd: Working directory for command execution
|
|
92
|
+
text: Whether to use text mode
|
|
93
|
+
input: Input to pass to command
|
|
94
|
+
capture_output: Whether to capture command output
|
|
95
|
+
timeout: Command execution timeout in seconds
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple containing (stdout, stderr, return_code)
|
|
99
|
+
"""
|
|
100
|
+
working_dir = cwd or self.cwd
|
|
101
|
+
self.active_processes = []
|
|
102
|
+
timeout = timeout if timeout is not None else self.timeout
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Configure output pipes
|
|
106
|
+
stdout_pipe = subprocess.PIPE if capture_output else None
|
|
107
|
+
stderr_pipe = subprocess.PIPE if capture_output else None
|
|
108
|
+
|
|
109
|
+
# Create and start process
|
|
110
|
+
proc = subprocess.Popen(
|
|
111
|
+
command,
|
|
112
|
+
cwd=working_dir,
|
|
113
|
+
stdout=stdout_pipe,
|
|
114
|
+
stderr=stderr_pipe,
|
|
115
|
+
text=text
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Track process and its children
|
|
119
|
+
main_pid = proc.pid
|
|
120
|
+
self.active_processes = self._get_process_children(main_pid) + [Process(main_pid)]
|
|
121
|
+
|
|
122
|
+
# Wait for completion and get output
|
|
123
|
+
proc_output, proc_error = proc.communicate(input=input, timeout=timeout)
|
|
124
|
+
exit_code = proc.returncode
|
|
125
|
+
|
|
126
|
+
# Ensure output strings are not None
|
|
127
|
+
command_output = proc_output or ""
|
|
128
|
+
error_output = proc_error or ""
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
# Handle execution errors
|
|
132
|
+
command_output, error_output, exit_code = "", str(e), -1
|
|
133
|
+
finally:
|
|
134
|
+
# Always cleanup processes
|
|
135
|
+
self._cleanup_processes()
|
|
136
|
+
return command_output, error_output, exit_code
|
|
137
|
+
|
|
138
|
+
def _get_process_children(self, pid: int) -> List[Process]:
|
|
139
|
+
"""
|
|
140
|
+
Get child processes for given PID
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
pid: Parent process ID
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of child processes
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
parent_process = Process(pid)
|
|
150
|
+
return parent_process.children(recursive=True)
|
|
151
|
+
except NoSuchProcess:
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
def _cleanup_processes(self):
|
|
155
|
+
"""
|
|
156
|
+
Clean up all tracked processes
|
|
157
|
+
Attempts to terminate processes gracefully
|
|
158
|
+
"""
|
|
159
|
+
for process in self.active_processes:
|
|
160
|
+
try:
|
|
161
|
+
process.terminate()
|
|
162
|
+
process.wait(timeout=5)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
if not isinstance(e, NoSuchProcess):
|
|
165
|
+
logger.debug(f"Failed to terminate process {process}: {e}")
|
|
166
|
+
|
|
167
|
+
# Git operations
|
|
168
|
+
def git_clone(self, repo_url: str, target_dir: Optional[str] = None) -> Tuple[bool, str]:
|
|
169
|
+
"""
|
|
170
|
+
Clone a git repository
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
repo_url: Repository URL to clone
|
|
174
|
+
target_dir: Target directory for clone
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Tuple of (success_status, error_message)
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
git.Repo.clone_from(repo_url, target_dir or os.path.basename(repo_url.split('/')[-1].split('.')[0]))
|
|
181
|
+
return True, ""
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return False, str(e)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: leanup
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Python package for Lean Environment Management
|
|
5
5
|
Author-email: Lean-zh Community <leanprover@outlook.com>
|
|
6
6
|
Maintainer-email: Lean-zh Community <leanprover@outlook.com>
|
|
@@ -10,6 +10,11 @@ Project-URL: changelog, https://github.com/{{ cookiecutter.github_username }}/{{
|
|
|
10
10
|
Project-URL: homepage, https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: gitpython
|
|
14
|
+
Requires-Dist: psutil
|
|
15
|
+
Requires-Dist: click
|
|
16
|
+
Requires-Dist: platformdirs
|
|
17
|
+
Requires-Dist: loguru
|
|
13
18
|
Provides-Extra: dev
|
|
14
19
|
Requires-Dist: coverage; extra == "dev"
|
|
15
20
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -23,8 +28,8 @@ Dynamic: license-file
|
|
|
23
28
|
<a href="https://pypi.python.org/pypi/leanup">
|
|
24
29
|
<img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
|
|
25
30
|
</a>
|
|
26
|
-
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
27
|
-
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.
|
|
31
|
+
<a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
|
|
32
|
+
<img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
|
|
28
33
|
</a>
|
|
29
34
|
<a href="https://codecov.io/gh/Lean-zh/leanup">
|
|
30
35
|
<img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
./leanup/__init__.py
|
|
5
|
+
./leanup/cli.py
|
|
6
|
+
./leanup/const.py
|
|
7
|
+
./leanup/leanup.py
|
|
8
|
+
./leanup/utils/__init__.py
|
|
9
|
+
./leanup/utils/executor.py
|
|
10
|
+
./tests/__init__.py
|
|
11
|
+
./tests/conftest.py
|
|
12
|
+
./tests/test_executor.py
|
|
13
|
+
./tests/test_leanup.py
|
|
14
|
+
leanup/__init__.py
|
|
15
|
+
leanup/cli.py
|
|
16
|
+
leanup/const.py
|
|
17
|
+
leanup/leanup.py
|
|
18
|
+
leanup.egg-info/PKG-INFO
|
|
19
|
+
leanup.egg-info/SOURCES.txt
|
|
20
|
+
leanup.egg-info/dependency_links.txt
|
|
21
|
+
leanup.egg-info/entry_points.txt
|
|
22
|
+
leanup.egg-info/requires.txt
|
|
23
|
+
leanup.egg-info/top_level.txt
|
|
24
|
+
leanup/utils/__init__.py
|
|
25
|
+
leanup/utils/executor.py
|
|
26
|
+
tests/__init__.py
|
|
27
|
+
tests/conftest.py
|
|
28
|
+
tests/test_executor.py
|
|
29
|
+
tests/test_leanup.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "leanup"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
description = "Python package for Lean Environment Management"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -18,7 +18,11 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
license = {text = "MIT license"}
|
|
20
20
|
dependencies = [
|
|
21
|
-
|
|
21
|
+
"gitpython",
|
|
22
|
+
"psutil", # Process management
|
|
23
|
+
"click", # Command line interface
|
|
24
|
+
"platformdirs", # Platform-specific directories
|
|
25
|
+
"loguru", # Logging
|
|
22
26
|
]
|
|
23
27
|
|
|
24
28
|
[project.optional-dependencies]
|
|
@@ -29,6 +33,9 @@ dev = [
|
|
|
29
33
|
"ruff" # linting
|
|
30
34
|
]
|
|
31
35
|
|
|
36
|
+
[project.scripts]
|
|
37
|
+
leanup = "leanup.cli:main"
|
|
38
|
+
|
|
32
39
|
[project.urls]
|
|
33
40
|
|
|
34
41
|
bugs = "https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/issues"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import pytest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from leanup.const import LEANUP_CACHE_DIR
|
|
5
|
+
from leanup.utils import CommandExecutor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def cache_dir():
|
|
10
|
+
"""Fixture to provide the LeanUp cache directory."""
|
|
11
|
+
return LEANUP_CACHE_DIR
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def executor():
|
|
15
|
+
"""Create a CommandExecutor instance for testing"""
|
|
16
|
+
return CommandExecutor()
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def temp_dir():
|
|
20
|
+
"""Create a temporary directory for testing"""
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
22
|
+
yield Path(tmp_dir)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import tempfile
|
|
5
|
+
from leanup.const import OS_TYPE
|
|
6
|
+
from leanup.utils.executor import CommandExecutor
|
|
7
|
+
|
|
8
|
+
def test_basic_execute(executor:CommandExecutor):
|
|
9
|
+
"""Test basic command execution"""
|
|
10
|
+
cmd = ['echo', 'hello']
|
|
11
|
+
|
|
12
|
+
output, error, code = executor.execute(cmd)
|
|
13
|
+
assert code == 0
|
|
14
|
+
assert 'hello' in output
|
|
15
|
+
assert error == ''
|
|
16
|
+
|
|
17
|
+
def test_execute_with_error(executor:CommandExecutor):
|
|
18
|
+
"""Test command execution with error"""
|
|
19
|
+
if OS_TYPE == 'Windows':
|
|
20
|
+
cmd = ['dir', '/invalid_path']
|
|
21
|
+
else:
|
|
22
|
+
cmd = ['ls', '/nonexistent_directory']
|
|
23
|
+
|
|
24
|
+
output, error, code = executor.execute(cmd)
|
|
25
|
+
assert code != 0
|
|
26
|
+
assert error != ''
|
|
27
|
+
|
|
28
|
+
def test_working_directory(executor:CommandExecutor, temp_dir:Path):
|
|
29
|
+
"""Test working directory context manager"""
|
|
30
|
+
test_file = temp_dir / 'test.txt'
|
|
31
|
+
test_file.write_text('test content')
|
|
32
|
+
|
|
33
|
+
with executor.working_directory(temp_dir, chdir=True):
|
|
34
|
+
if OS_TYPE == 'Windows':
|
|
35
|
+
cmd = ['dir']
|
|
36
|
+
else:
|
|
37
|
+
cmd = ['ls']
|
|
38
|
+
output, error, code = executor.execute(cmd)
|
|
39
|
+
assert code == 0
|
|
40
|
+
assert 'test.txt' in output
|
|
41
|
+
|
|
42
|
+
def test_execute_in_directory(executor:CommandExecutor, temp_dir:Path):
|
|
43
|
+
"""Test execute_in_directory method"""
|
|
44
|
+
test_file = temp_dir / 'test.txt'
|
|
45
|
+
test_file.write_text('test content')
|
|
46
|
+
|
|
47
|
+
if OS_TYPE == 'Windows':
|
|
48
|
+
cmd = ['dir']
|
|
49
|
+
else:
|
|
50
|
+
cmd = ['ls']
|
|
51
|
+
|
|
52
|
+
output, error, code = executor.execute_in_directory(cmd, directory=temp_dir)
|
|
53
|
+
assert code == 0
|
|
54
|
+
assert 'test.txt' in output
|
|
55
|
+
|
|
56
|
+
def test_timeout(executor:CommandExecutor):
|
|
57
|
+
"""Test command execution with timeout"""
|
|
58
|
+
if OS_TYPE == 'Windows':
|
|
59
|
+
cmd = ['timeout', '10']
|
|
60
|
+
else:
|
|
61
|
+
cmd = ['sleep', '10']
|
|
62
|
+
|
|
63
|
+
output, error, code = executor.execute(cmd, timeout=1)
|
|
64
|
+
assert code != 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_multiple_commands(executor:CommandExecutor):
|
|
68
|
+
"""Test executing multiple commands sequentially"""
|
|
69
|
+
commands = []
|
|
70
|
+
if OS_TYPE == 'Windows':
|
|
71
|
+
commands = [
|
|
72
|
+
['echo', 'first'],
|
|
73
|
+
['echo', 'second'],
|
|
74
|
+
]
|
|
75
|
+
else:
|
|
76
|
+
commands = [
|
|
77
|
+
['echo', 'first'],
|
|
78
|
+
['echo', 'second'],
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
results = []
|
|
82
|
+
for cmd in commands:
|
|
83
|
+
output, error, code = executor.execute(cmd)
|
|
84
|
+
results.append((output, code))
|
|
85
|
+
|
|
86
|
+
assert all(code == 0 for _, code in results)
|
|
87
|
+
assert 'first' in results[0][0]
|
|
88
|
+
assert 'second' in results[1][0]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""Tests for `leanup` package."""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from leanup.const import LEANUP_CACHE_DIR, OS_TYPE
|
|
7
|
+
|
|
8
|
+
def test_leanup_basic():
|
|
9
|
+
"""Test if the package can be imported."""
|
|
10
|
+
from leanup import __version__
|
|
11
|
+
print(f"LeanUp version: {__version__}")
|
|
12
|
+
print(f"LeanUp cache directory: {LEANUP_CACHE_DIR}")
|
|
13
|
+
|
|
14
|
+
def test_system():
|
|
15
|
+
"""Test if the computer system is recognized."""
|
|
16
|
+
assert OS_TYPE in ['Windows', 'MacOS', 'Linux'], f"Unexpected OS_TYPE: {OS_TYPE}"
|
|
17
|
+
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from .install import (
|
|
2
|
-
install_repl, install_lean_repo, install_lean, install_mathlib, get_valid_versions
|
|
3
|
-
)
|
|
4
|
-
from .utils import (
|
|
5
|
-
list_lookeng_cache, list_mathlib_cache, list_repo_cache,
|
|
6
|
-
read_lookeng_cache, read_mathlib_cache, read_repo_cache,
|
|
7
|
-
get_lookeng_versions, get_mathlib_versions
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
help_message = """
|
|
12
|
-
Lean Repo management CLI
|
|
13
|
-
|
|
14
|
-
A tool for managing Lean repositories.
|
|
15
|
-
"""
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
from lookeng.utils import working_directory, execute_command, execute_popen_command
|
|
2
|
-
from lookeng.constants import MATHLIB_URL
|
|
3
|
-
from lookeng import constants
|
|
4
|
-
import os
|
|
5
|
-
import toml, subprocess, shutil, re
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Tuple, Optional, List
|
|
8
|
-
from loguru import logger
|
|
9
|
-
from git import Repo, BadName
|
|
10
|
-
from .utils import (
|
|
11
|
-
url_to_repo_name, install_lean, get_valid_versions, get_tag_sha,
|
|
12
|
-
detect_lakefile, update_lakefile_toml, update_lakefile_lean, url_to_prefix
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
class InstallationError(Exception):
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
def install_lean_repo(url:str, version: str,
|
|
19
|
-
prefix:Optional[str]=None,
|
|
20
|
-
force: bool = False,
|
|
21
|
-
dest_dir: Optional[str] = None,
|
|
22
|
-
with_mathlib:bool=False,
|
|
23
|
-
build_cmds:Optional[list[str]]=None) -> Path:
|
|
24
|
-
"""
|
|
25
|
-
Download and build Lean 4 Repo.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
url: Lean repository URL
|
|
29
|
-
version: Lean tag (e.g. 'v4.10.0')
|
|
30
|
-
prefix: Prefix for the installation directory, extract from url by default
|
|
31
|
-
force: If True, overwrite existing installation
|
|
32
|
-
dest_dir: Custom installation directory
|
|
33
|
-
build_cmds: List of build commands to run
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Path: Installation directory path
|
|
37
|
-
|
|
38
|
-
Raises:
|
|
39
|
-
InstallationError: If installation fails
|
|
40
|
-
"""
|
|
41
|
-
# make sure `elan`` is installed
|
|
42
|
-
if shutil.which('lake') is None:
|
|
43
|
-
install_lean()
|
|
44
|
-
os.environ['PATH'] += os.pathsep + os.path.expanduser('~/.elan/bin')
|
|
45
|
-
# default build commands
|
|
46
|
-
if build_cmds is None:
|
|
47
|
-
build_cmds = ['lake update -R', 'lake build']
|
|
48
|
-
# Setup paths
|
|
49
|
-
dest_dir = dest_dir and Path(dest_dir).resolve()
|
|
50
|
-
if dest_dir is not None and not force and dest_dir.exists():
|
|
51
|
-
logger.info(f"Destination directory {dest_dir} already exists")
|
|
52
|
-
return dest_dir
|
|
53
|
-
|
|
54
|
-
# Check existing cache
|
|
55
|
-
username, repo_name = url_to_repo_name(url)
|
|
56
|
-
prefix = prefix or url_to_prefix(url)
|
|
57
|
-
cache_dir = Path(constants.LOOKENG_CACHE_DIR)
|
|
58
|
-
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
cache_path = cache_dir / f"{prefix}-{version}"
|
|
60
|
-
|
|
61
|
-
if cache_path.exists() and not force:
|
|
62
|
-
logger.info(f"Version {version} already installed at {cache_path}")
|
|
63
|
-
if dest_dir is not None:
|
|
64
|
-
_, err, code = execute_command(["cp", "-r", cache_path, dest_dir])
|
|
65
|
-
if code != 0:
|
|
66
|
-
raise InstallationError(f"Failed to copy cache to destination directory: {err}")
|
|
67
|
-
return dest_dir
|
|
68
|
-
return cache_path
|
|
69
|
-
|
|
70
|
-
work_dir = dest_dir and dest_dir.parent # set None if not specified
|
|
71
|
-
with working_directory(work_dir, chdir=True) as work_dir:
|
|
72
|
-
if dest_dir is None: # use temp dir
|
|
73
|
-
repo_path = Path(work_dir) / f"{prefix}-{version}"
|
|
74
|
-
logger.debug(f"Working in temporary directory: {work_dir}")
|
|
75
|
-
else:
|
|
76
|
-
repo_path = dest_dir
|
|
77
|
-
logger.debug(f"Working in directory: {work_dir}")
|
|
78
|
-
if repo_path.exists(): # remove existing directory
|
|
79
|
-
shutil.rmtree(repo_path)
|
|
80
|
-
cache_repo_base = cache_dir / url_to_prefix(url) # track the latest repo
|
|
81
|
-
# Create new repo
|
|
82
|
-
try:
|
|
83
|
-
if not (version in get_valid_versions(url, update=False) or version in get_valid_versions(url, update=False)):
|
|
84
|
-
raise InstallationError(f"Unknown version: {version}")
|
|
85
|
-
Repo.clone_from(str(cache_repo_base), repo_path, branch=version)
|
|
86
|
-
except BadName:
|
|
87
|
-
raise InstallationError(f"Version {version} not found in repository")
|
|
88
|
-
if with_mathlib and repo_name not in ['mathlib', 'mathlib4']:
|
|
89
|
-
# update lakefile
|
|
90
|
-
lakefile, file_type = detect_lakefile(repo_path)
|
|
91
|
-
mathlib_rev = get_tag_sha(MATHLIB_URL, version)
|
|
92
|
-
logger.debug(f"Detected {file_type} lakefile at {lakefile}")
|
|
93
|
-
if file_type == 'toml':
|
|
94
|
-
update_lakefile_toml(lakefile, mathlib_rev)
|
|
95
|
-
else:
|
|
96
|
-
update_lakefile_lean(lakefile, version)
|
|
97
|
-
for cmd in build_cmds:
|
|
98
|
-
logger.info(f"Running command: {cmd}")
|
|
99
|
-
_, err, code = execute_command(cmd.split(), cwd=repo_path, capture_output=False)
|
|
100
|
-
if code != 0:
|
|
101
|
-
raise InstallationError(f"Failed to run {cmd}: {err}")
|
|
102
|
-
|
|
103
|
-
# Save to cache
|
|
104
|
-
if cache_path.exists():
|
|
105
|
-
shutil.rmtree(cache_path)
|
|
106
|
-
logger.debug(f"Saving installation to cache: {cache_path}")
|
|
107
|
-
_, err, code = execute_command(["mv", repo_path, cache_path])
|
|
108
|
-
if code != 0:
|
|
109
|
-
raise InstallationError(f"Failed to save installation to cache: {err}")
|
|
110
|
-
if dest_dir is None:
|
|
111
|
-
repo_path = cache_path # return the cache path
|
|
112
|
-
logger.info(f"Setup completed successfully in {repo_path}")
|
|
113
|
-
return repo_path
|
|
114
|
-
|
|
115
|
-
def install_mathlib(version: str,
|
|
116
|
-
force: bool = False,
|
|
117
|
-
dest_dir: Optional[str] = None):
|
|
118
|
-
"""Install mathlib4."""
|
|
119
|
-
build_cmds = ['lake exe cache get', 'lake build']
|
|
120
|
-
return install_lean_repo(MATHLIB_URL, version, prefix='mathlib', force=force, dest_dir=dest_dir, build_cmds=build_cmds)
|
|
121
|
-
|
|
122
|
-
# set up REPL
|
|
123
|
-
def install_repl( version: str
|
|
124
|
-
, force: bool = False
|
|
125
|
-
, dest_dir:Optional[Path] = None
|
|
126
|
-
, url:Optional[str]=None) -> Path:
|
|
127
|
-
"""
|
|
128
|
-
Install specified version of Lean REPL
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
version: REPL version to install, None for latest
|
|
132
|
-
force: Force reinstall if already exists
|
|
133
|
-
dest_dir: Destination directory for installation
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
Path to installed REPL
|
|
137
|
-
|
|
138
|
-
Raises:
|
|
139
|
-
InstallationError: If installation fails
|
|
140
|
-
"""
|
|
141
|
-
if url is None:
|
|
142
|
-
url = 'https://github.com/leanprover-community/repl'
|
|
143
|
-
return install_lean_repo(url, version, prefix='repl', force=force, dest_dir=dest_dir, with_mathlib=True)
|