cli2 5.2.1rc1__tar.gz → 5.2.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.
- {cli2-5.2.1rc1/cli2.egg-info → cli2-5.2.3}/PKG-INFO +3 -2
- {cli2-5.2.1rc1 → cli2-5.2.3}/README.rst +2 -1
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/__init__.py +3 -1
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/asyncio.py +0 -17
- cli2-5.2.3/cli2/find.py +189 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/interactive.py +32 -16
- cli2-5.2.3/cli2/proc.py +302 -0
- {cli2-5.2.1rc1 → cli2-5.2.3/cli2.egg-info}/PKG-INFO +3 -2
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2.egg-info/SOURCES.txt +4 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/setup.py +1 -1
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_asyncio.py +0 -15
- cli2-5.2.3/tests/test_find.py +131 -0
- cli2-5.2.3/tests/test_proc.py +150 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_prompt2.py +78 -12
- {cli2-5.2.1rc1 → cli2-5.2.3}/MANIFEST.in +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/classifiers.txt +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/cli.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/cli2.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/colors.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/configuration.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/decorators.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/display.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/__init__.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/conf.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/example.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/example_obj.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/nesting.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/obj.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/obj2.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/examples/test.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/lock.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/log.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/mask.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/node.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/notlevenshtein.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/sphinx.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/table.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/test.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2/theme.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2.egg-info/requires.txt +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/cli2.egg-info/top_level.txt +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/setup.cfg +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_ansible.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_ansible_variables.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_cli.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_client.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_client_test.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_command.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_configuration.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_decorators.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_display.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_entry_point.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_group.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_inject.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_interactive.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_lock.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_log.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_mask.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_node.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_notlevenshtein.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_restful.py +0 -0
- {cli2-5.2.1rc1 → cli2-5.2.3}/tests/test_table.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.3
|
|
4
4
|
Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
|
|
5
5
|
Home-page: https://yourlabs.io/oss/cli2
|
|
6
6
|
Author: James Pic
|
|
@@ -63,6 +63,7 @@ Batteries included, all of which are useful on their own:
|
|
|
63
63
|
testing library so that you can go straight to the point in pytest
|
|
64
64
|
- a good old fcntl based locking
|
|
65
65
|
- a command line to run any python function over a beautiful CLI
|
|
66
|
-
- **AI
|
|
66
|
+
- **AI CLI with prompt2**
|
|
67
|
+
- **AI coding with code2** (TBA)
|
|
67
68
|
|
|
68
69
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|
|
@@ -25,6 +25,7 @@ Batteries included, all of which are useful on their own:
|
|
|
25
25
|
testing library so that you can go straight to the point in pytest
|
|
26
26
|
- a good old fcntl based locking
|
|
27
27
|
- a command line to run any python function over a beautiful CLI
|
|
28
|
-
- **AI
|
|
28
|
+
- **AI CLI with prompt2**
|
|
29
|
+
- **AI coding with code2** (TBA)
|
|
29
30
|
|
|
30
31
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|
|
@@ -11,7 +11,7 @@ from .cli import (
|
|
|
11
11
|
Cli2Error,
|
|
12
12
|
Cli2ValueError,
|
|
13
13
|
)
|
|
14
|
-
from .asyncio import async_resolve,
|
|
14
|
+
from .asyncio import async_resolve, Queue
|
|
15
15
|
from .colors import colors as c
|
|
16
16
|
from .theme import theme, t
|
|
17
17
|
|
|
@@ -28,6 +28,8 @@ else:
|
|
|
28
28
|
from .log import configure, log, parse
|
|
29
29
|
from .mask import Mask
|
|
30
30
|
from .notlevenshtein import closest, closest_path
|
|
31
|
+
from .proc import Proc
|
|
32
|
+
from .find import Find
|
|
31
33
|
from .table import Table
|
|
32
34
|
|
|
33
35
|
|
|
@@ -39,23 +39,6 @@ async def async_resolve(result, output=False):
|
|
|
39
39
|
return result
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def async_run(coroutine):
|
|
43
|
-
"""Run an async coroutine in the current event loop or create a new one.
|
|
44
|
-
|
|
45
|
-
If an event loop is already running, creates a task in that loop.
|
|
46
|
-
If no event loop is running, creates a new one and runs the coroutine.
|
|
47
|
-
|
|
48
|
-
:param coroutine: The coroutine to run (return value of an async function)
|
|
49
|
-
:return: The result of the coroutine execution
|
|
50
|
-
"""
|
|
51
|
-
try:
|
|
52
|
-
loop = asyncio.get_running_loop()
|
|
53
|
-
except RuntimeError:
|
|
54
|
-
return asyncio.run(coroutine)
|
|
55
|
-
else:
|
|
56
|
-
return loop.create_task(coroutine)
|
|
57
|
-
|
|
58
|
-
|
|
59
42
|
class Queue(asyncio.Queue):
|
|
60
43
|
"""
|
|
61
44
|
An async queue with worker pool for concurrent task processing.
|
cli2-5.2.3/cli2/find.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Effecient git-aware file finder with filtering capabilities.
|
|
3
|
+
|
|
4
|
+
Uses Linux commands: find, comm and git-ignore for an efficient path walker.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
|
|
8
|
+
.. code-block:: python
|
|
9
|
+
|
|
10
|
+
# Simple usage with default root
|
|
11
|
+
finder = cli2.Find()
|
|
12
|
+
files = finder.files()
|
|
13
|
+
dirs = finder.dirs()
|
|
14
|
+
|
|
15
|
+
# Usage with filters and callback
|
|
16
|
+
def callback(filepath):
|
|
17
|
+
print(f"Found: {filepath}")
|
|
18
|
+
|
|
19
|
+
finder = cli2.Find(
|
|
20
|
+
root="/path/to/repo",
|
|
21
|
+
glob_include=['*.py'],
|
|
22
|
+
glob_exclude=['*test*'],
|
|
23
|
+
file_callback=callback
|
|
24
|
+
)
|
|
25
|
+
files = finder.files()
|
|
26
|
+
dirs = finder.dirs()
|
|
27
|
+
"""
|
|
28
|
+
import cli2
|
|
29
|
+
from fnmatch import fnmatch
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Find:
|
|
35
|
+
"""
|
|
36
|
+
A class to walk through files and directories not ignored by git with
|
|
37
|
+
optional filtering.
|
|
38
|
+
|
|
39
|
+
.. py:attribute:: root
|
|
40
|
+
|
|
41
|
+
Root directory for file operations
|
|
42
|
+
|
|
43
|
+
.. py:attribute:: glob_include
|
|
44
|
+
|
|
45
|
+
Optional list of glob patterns to include
|
|
46
|
+
|
|
47
|
+
.. py:attribute:: glob_exclude
|
|
48
|
+
|
|
49
|
+
Optional list of glob patterns to exclude
|
|
50
|
+
|
|
51
|
+
.. py:attribute:: file_callback
|
|
52
|
+
|
|
53
|
+
Optional callback function called for each file or directory
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
root=None,
|
|
59
|
+
glob_include=None,
|
|
60
|
+
glob_exclude=None,
|
|
61
|
+
file_callback=None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Initialize Find with optional root directory, filters, and callback.
|
|
65
|
+
|
|
66
|
+
:param root: Root directory (defaults to current working directory if
|
|
67
|
+
not specified)
|
|
68
|
+
:type root: str or pathlib.Path or None
|
|
69
|
+
:param glob_include: List of glob patterns to include
|
|
70
|
+
:type glob_include: list or None
|
|
71
|
+
:param glob_exclude: List of glob patterns to exclude
|
|
72
|
+
:type glob_exclude: list or None
|
|
73
|
+
:param file_callback: Function to call for each file or directory
|
|
74
|
+
:type file_callback: callable or None
|
|
75
|
+
"""
|
|
76
|
+
self.root = Path(root if root is not None else os.getcwd()).resolve()
|
|
77
|
+
self.glob_include = glob_include if glob_include is not None else []
|
|
78
|
+
self.glob_exclude = glob_exclude if glob_exclude is not None else []
|
|
79
|
+
self.file_callback = file_callback
|
|
80
|
+
|
|
81
|
+
def _matches_filters(self, filepath):
|
|
82
|
+
"""
|
|
83
|
+
Check if a file or directory matches the include/exclude filters.
|
|
84
|
+
|
|
85
|
+
:param filepath: Path to check against filters
|
|
86
|
+
:type filepath: pathlib.Path
|
|
87
|
+
:return: True if path should be included, False otherwise
|
|
88
|
+
:rtype: bool
|
|
89
|
+
"""
|
|
90
|
+
filepath_str = str(filepath.relative_to(self.root))
|
|
91
|
+
|
|
92
|
+
if self.glob_include:
|
|
93
|
+
if not any(
|
|
94
|
+
fnmatch(filepath_str, pattern) for pattern in self.glob_include
|
|
95
|
+
):
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if self.glob_exclude:
|
|
99
|
+
if any(
|
|
100
|
+
fnmatch(filepath_str, pattern) for pattern in self.glob_exclude
|
|
101
|
+
):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def files(self, directory=None):
|
|
107
|
+
"""
|
|
108
|
+
List files not ignored by git, applying filters and callback.
|
|
109
|
+
|
|
110
|
+
:param directory: Directory to start search from (defaults to root if
|
|
111
|
+
not specified)
|
|
112
|
+
:type directory: str or pathlib.Path or None
|
|
113
|
+
:return: List of Path objects for files not ignored by git that match
|
|
114
|
+
filters
|
|
115
|
+
:rtype: list
|
|
116
|
+
:raises RuntimeError: If the git command fails
|
|
117
|
+
"""
|
|
118
|
+
base_path = Path(directory).resolve() if directory else self.root
|
|
119
|
+
|
|
120
|
+
cmd = ' '.join([
|
|
121
|
+
f'comm -23 <(find {base_path} -type f | sort)',
|
|
122
|
+
f'<(find {base_path} -type f | git check-ignore --stdin | sort)'
|
|
123
|
+
])
|
|
124
|
+
proc = cli2.Proc("bash", "-c", cmd).wait()
|
|
125
|
+
|
|
126
|
+
if proc.rc != 0:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"Command failed with return code {proc.rc}: {proc.stderr}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
files = []
|
|
132
|
+
for line in proc.stdout.splitlines():
|
|
133
|
+
if not line.strip():
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
filepath = (base_path / line.strip()).resolve()
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
not self.glob_include and not self.glob_exclude
|
|
140
|
+
) or self._matches_filters(filepath):
|
|
141
|
+
files.append(filepath)
|
|
142
|
+
if self.file_callback:
|
|
143
|
+
self.file_callback(filepath)
|
|
144
|
+
|
|
145
|
+
return files
|
|
146
|
+
|
|
147
|
+
def dirs(self, directory=None):
|
|
148
|
+
"""
|
|
149
|
+
List directories not ignored by git, applying filters and callback.
|
|
150
|
+
|
|
151
|
+
:param directory: Directory to start search from (defaults to root if
|
|
152
|
+
not specified)
|
|
153
|
+
:type directory: str or pathlib.Path or None
|
|
154
|
+
:return: List of Path objects for directories not ignored by git that
|
|
155
|
+
match filters
|
|
156
|
+
:rtype: list
|
|
157
|
+
:raises RuntimeError: If the git command fails
|
|
158
|
+
"""
|
|
159
|
+
base_path = Path(directory).resolve() if directory else self.root
|
|
160
|
+
|
|
161
|
+
cmd = ' '.join([
|
|
162
|
+
f'comm -23 <(find {base_path} -type d | sort)',
|
|
163
|
+
f'<(find {base_path} -type d | git check-ignore --stdin | sort)'
|
|
164
|
+
])
|
|
165
|
+
proc = cli2.Proc("bash", "-c", cmd).wait()
|
|
166
|
+
|
|
167
|
+
if proc.rc != 0:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"Command failed with return code {proc.rc}: {proc.stderr}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
dirs = []
|
|
173
|
+
for line in proc.stdout.splitlines():
|
|
174
|
+
if not line.strip():
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
dirpath = (base_path / line.strip()).resolve()
|
|
178
|
+
|
|
179
|
+
if dirpath == base_path:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
not self.glob_include and not self.glob_exclude
|
|
184
|
+
) or self._matches_filters(dirpath):
|
|
185
|
+
dirs.append(dirpath)
|
|
186
|
+
if self.file_callback:
|
|
187
|
+
self.file_callback(dirpath)
|
|
188
|
+
|
|
189
|
+
return dirs
|
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import shlex
|
|
4
4
|
import subprocess
|
|
5
5
|
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from .log import log
|
|
8
9
|
|
|
@@ -49,32 +50,47 @@ def editor(content=None):
|
|
|
49
50
|
|
|
50
51
|
Like git rebase -i does!
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
- If a file path is given, edit in place.
|
|
54
|
+
- Otherwise, write to a temporary file.
|
|
55
|
+
- Anyway: return the written contents.
|
|
56
|
+
|
|
57
|
+
:param content: Initial content if any, or a file path
|
|
53
58
|
:return: The edited content after $EDITOR exit
|
|
54
59
|
"""
|
|
55
|
-
tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".txt")
|
|
56
|
-
with tmp as f:
|
|
57
|
-
f.write(content)
|
|
58
|
-
f.flush()
|
|
59
|
-
filepath = f.name
|
|
60
|
-
|
|
61
60
|
editor = os.getenv('EDITOR', 'vim')
|
|
62
61
|
|
|
62
|
+
if Path(content).exists():
|
|
63
|
+
# open directly on target path
|
|
64
|
+
filepath = content
|
|
65
|
+
tmp = None
|
|
66
|
+
else:
|
|
67
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
68
|
+
mode='w+',
|
|
69
|
+
delete=False,
|
|
70
|
+
suffix=".txt",
|
|
71
|
+
)
|
|
72
|
+
with tmp as f:
|
|
73
|
+
f.write(content)
|
|
74
|
+
f.flush()
|
|
75
|
+
filepath = f.name
|
|
76
|
+
|
|
77
|
+
command = f"{editor} {shlex.quote(str(filepath))}"
|
|
78
|
+
|
|
63
79
|
try:
|
|
64
|
-
command = f"{editor} {shlex.quote(filepath)}"
|
|
65
80
|
subprocess.run(shlex.split(command), check=True)
|
|
66
|
-
|
|
67
|
-
with open(filepath, 'r') as f:
|
|
68
|
-
content = f.read()
|
|
69
|
-
return content
|
|
70
81
|
except subprocess.CalledProcessError as e:
|
|
71
82
|
log.error(f"Error running Vim: {e}")
|
|
72
83
|
return None
|
|
73
84
|
except FileNotFoundError:
|
|
74
85
|
log.warn(f"Temporary file gone?? {filepath}")
|
|
75
86
|
return None
|
|
87
|
+
else:
|
|
88
|
+
with open(filepath, 'r') as f:
|
|
89
|
+
content = f.read()
|
|
90
|
+
return content
|
|
76
91
|
finally:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
if tmp:
|
|
93
|
+
try:
|
|
94
|
+
os.remove(filepath)
|
|
95
|
+
except OSError as e:
|
|
96
|
+
log.warn(f"Error deleting temporary file {filepath}: {e}")
|
cli2-5.2.3/cli2/proc.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asyncio subprocess wrapper featuring:
|
|
3
|
+
|
|
4
|
+
- Capture + live logging of stdout/stderr
|
|
5
|
+
- ANSI escape code cleaning for captured output: print colored output for
|
|
6
|
+
humans, have clean output in a variable for processing, log, cache... and
|
|
7
|
+
sending to LLMs!
|
|
8
|
+
- Separate start/wait methods for process control
|
|
9
|
+
|
|
10
|
+
Example usage:
|
|
11
|
+
|
|
12
|
+
.. code-block:: python
|
|
13
|
+
|
|
14
|
+
# pass shell command in a string for convenience
|
|
15
|
+
proc = cli2.Proc('foo bar')
|
|
16
|
+
|
|
17
|
+
# or as list, better when building commands
|
|
18
|
+
proc = cli2.Proc('foo', 'bar')
|
|
19
|
+
|
|
20
|
+
# run in sync mode (ie. for jinja2)
|
|
21
|
+
proc.wait()
|
|
22
|
+
|
|
23
|
+
# OR run in async loop
|
|
24
|
+
await proc.waita()
|
|
25
|
+
|
|
26
|
+
# You can chain
|
|
27
|
+
proc = cli2.Proc('hi').wait()
|
|
28
|
+
proc = await cli2.Proc('hi').waita()
|
|
29
|
+
|
|
30
|
+
.. note:: There are also start functions, sync and async, in case you want to
|
|
31
|
+
start the proc and wait later.
|
|
32
|
+
"""
|
|
33
|
+
import asyncio
|
|
34
|
+
import os
|
|
35
|
+
import shlex
|
|
36
|
+
import re
|
|
37
|
+
import subprocess
|
|
38
|
+
|
|
39
|
+
from .log import log
|
|
40
|
+
|
|
41
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Proc:
|
|
45
|
+
"""
|
|
46
|
+
Asynchronous subprocess manager with advanced IO handling.
|
|
47
|
+
|
|
48
|
+
.. py:attribute:: args
|
|
49
|
+
|
|
50
|
+
Full command arguments list used to launch the process
|
|
51
|
+
|
|
52
|
+
.. py:attribute:: rc
|
|
53
|
+
|
|
54
|
+
Return Code: process exit code (available after process completes)
|
|
55
|
+
|
|
56
|
+
.. py:attribute:: out
|
|
57
|
+
|
|
58
|
+
Combined cleaned output with ANSI escape codes removed.
|
|
59
|
+
|
|
60
|
+
.. py:attribute:: out_ansi
|
|
61
|
+
|
|
62
|
+
Combined stdout/stderr output with ANSI codes preserved.
|
|
63
|
+
|
|
64
|
+
.. py:attribute:: stdout
|
|
65
|
+
|
|
66
|
+
Cleaned stdout output with ANSI escape codes removed.
|
|
67
|
+
|
|
68
|
+
.. py:attribute:: stderr
|
|
69
|
+
|
|
70
|
+
Cleaned stdout output with ANSI escape codes removed.
|
|
71
|
+
|
|
72
|
+
.. py:attribute:: stdout_ansi
|
|
73
|
+
|
|
74
|
+
Stdout output with ANSI escape codes preserved.
|
|
75
|
+
|
|
76
|
+
.. py:attribute:: stderr_ansi
|
|
77
|
+
|
|
78
|
+
Stderr output with ANSI escape codes preserved.
|
|
79
|
+
"""
|
|
80
|
+
def __init__(self, cmd, *args, quiet=False, inherit=True, timeout=None,
|
|
81
|
+
**env):
|
|
82
|
+
"""
|
|
83
|
+
:param cmd: Command string (will shlex split) or initial argument
|
|
84
|
+
:param args: Additional command arguments
|
|
85
|
+
:param quiet: Suppress live output printing (default: False)
|
|
86
|
+
:param inherit: Inherit parent environment variables (default: True)
|
|
87
|
+
:param timeout: Maximum execution time in seconds (default: None)
|
|
88
|
+
:param env: Additional environment variables to set
|
|
89
|
+
:type env: Environment variables.
|
|
90
|
+
"""
|
|
91
|
+
if args:
|
|
92
|
+
self.args = [cmd] + list(args)
|
|
93
|
+
else:
|
|
94
|
+
self.args = shlex.split(cmd)
|
|
95
|
+
|
|
96
|
+
self.quiet = quiet
|
|
97
|
+
|
|
98
|
+
self.env = dict()
|
|
99
|
+
if inherit:
|
|
100
|
+
self.env = os.environ.copy()
|
|
101
|
+
self.env.update(env)
|
|
102
|
+
|
|
103
|
+
self.out_raw = bytearray()
|
|
104
|
+
self.err_raw = bytearray()
|
|
105
|
+
self.raw = bytearray()
|
|
106
|
+
|
|
107
|
+
self.started = False
|
|
108
|
+
self.waited = False
|
|
109
|
+
self.timeout = timeout
|
|
110
|
+
self.rc = None
|
|
111
|
+
self.proc = None
|
|
112
|
+
|
|
113
|
+
def clone(self):
|
|
114
|
+
"""
|
|
115
|
+
Create a new unstarted Proc instance with identical configuration.
|
|
116
|
+
|
|
117
|
+
:return: New Proc instance ready for execution
|
|
118
|
+
"""
|
|
119
|
+
return type(self)(
|
|
120
|
+
*self.args, quiet=self.quiet, inherit=True, timeout=self.timeout,
|
|
121
|
+
**self.env
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def cmd(self):
|
|
126
|
+
"""
|
|
127
|
+
Get/set the command as a shell-joinable string.
|
|
128
|
+
|
|
129
|
+
:getter: Returns shell-escaped command string
|
|
130
|
+
:setter: Parses and updates internal args list
|
|
131
|
+
:type: str
|
|
132
|
+
"""
|
|
133
|
+
return shlex.join(self.args)
|
|
134
|
+
|
|
135
|
+
@cmd.setter
|
|
136
|
+
def cmd(self, value):
|
|
137
|
+
self.args = shlex.split(value)
|
|
138
|
+
|
|
139
|
+
async def starta(self):
|
|
140
|
+
"""
|
|
141
|
+
Launch the subprocess asynchronously.
|
|
142
|
+
|
|
143
|
+
:return: Self reference for method chaining
|
|
144
|
+
:raises RuntimeError: If process is already started
|
|
145
|
+
"""
|
|
146
|
+
if self.started:
|
|
147
|
+
raise RuntimeError("Process already started")
|
|
148
|
+
|
|
149
|
+
if not self.quiet:
|
|
150
|
+
log.debug('cmd', cmd=self.cmd)
|
|
151
|
+
|
|
152
|
+
self.proc = await asyncio.create_subprocess_exec(
|
|
153
|
+
*[str(arg) for arg in self.args],
|
|
154
|
+
stdin=asyncio.subprocess.PIPE,
|
|
155
|
+
stdout=asyncio.subprocess.PIPE,
|
|
156
|
+
stderr=asyncio.subprocess.PIPE,
|
|
157
|
+
env={str(k): str(v) for k, v in self.env.items()},
|
|
158
|
+
)
|
|
159
|
+
self.started = True
|
|
160
|
+
|
|
161
|
+
self.stdout_task = asyncio.create_task(
|
|
162
|
+
self._handle_output(self.proc.stdout, 1)
|
|
163
|
+
)
|
|
164
|
+
self.stderr_task = asyncio.create_task(
|
|
165
|
+
self._handle_output(self.proc.stderr, 2)
|
|
166
|
+
)
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
async def waita(self):
|
|
170
|
+
"""
|
|
171
|
+
Wait for process completion with timeout handling.
|
|
172
|
+
|
|
173
|
+
Terminates process if timeout occurs. Gathers all output streams.
|
|
174
|
+
|
|
175
|
+
:return: Self reference for method chaining
|
|
176
|
+
"""
|
|
177
|
+
if not self.started:
|
|
178
|
+
await self.starta()
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
if self.timeout:
|
|
182
|
+
await asyncio.wait_for(self.proc.wait(), timeout=self.timeout)
|
|
183
|
+
else:
|
|
184
|
+
await self.proc.wait()
|
|
185
|
+
except asyncio.TimeoutError:
|
|
186
|
+
print(f"Process timed out after {self.timeout}s")
|
|
187
|
+
self.proc.terminate()
|
|
188
|
+
await self.proc.wait()
|
|
189
|
+
|
|
190
|
+
await asyncio.gather(self.stdout_task, self.stderr_task)
|
|
191
|
+
self.rc = self.proc.returncode
|
|
192
|
+
self.waited = True
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
async def _handle_output(self, stream, fd):
|
|
196
|
+
"""
|
|
197
|
+
Internal method for stream handling.
|
|
198
|
+
|
|
199
|
+
:param stream: Output stream to monitor
|
|
200
|
+
:type stream: asyncio.StreamReader
|
|
201
|
+
:param fd: Stream identifier (1=stdout, 2=stderr)
|
|
202
|
+
:type fd: int
|
|
203
|
+
"""
|
|
204
|
+
while True:
|
|
205
|
+
line = await stream.readline()
|
|
206
|
+
if not line: # EOF
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
decoded_line = line.decode().rstrip()
|
|
210
|
+
if fd == 1: # stdout
|
|
211
|
+
self.out_raw.extend(line)
|
|
212
|
+
elif fd == 2: # stderr
|
|
213
|
+
self.err_raw.extend(line)
|
|
214
|
+
self.raw.extend(line)
|
|
215
|
+
|
|
216
|
+
if not self.quiet:
|
|
217
|
+
print(decoded_line)
|
|
218
|
+
|
|
219
|
+
def start(self):
|
|
220
|
+
"""
|
|
221
|
+
Start the subprocess synchronously without waiting for output.
|
|
222
|
+
"""
|
|
223
|
+
if self.started:
|
|
224
|
+
raise RuntimeError("Process already started")
|
|
225
|
+
|
|
226
|
+
if not self.quiet:
|
|
227
|
+
log.debug('cmd', cmd=self.cmd)
|
|
228
|
+
|
|
229
|
+
self.proc = subprocess.Popen(
|
|
230
|
+
self.args,
|
|
231
|
+
stdin=subprocess.PIPE,
|
|
232
|
+
stdout=subprocess.PIPE,
|
|
233
|
+
stderr=subprocess.PIPE,
|
|
234
|
+
env=self.env,
|
|
235
|
+
universal_newlines=True
|
|
236
|
+
)
|
|
237
|
+
self.started = True
|
|
238
|
+
# Do NOT start output handling here; defer to wait
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
def wait(self):
|
|
242
|
+
"""
|
|
243
|
+
Wait for process completion synchronously with timeout handling.
|
|
244
|
+
Collects output streams after waiting.
|
|
245
|
+
"""
|
|
246
|
+
if not self.started:
|
|
247
|
+
self.start()
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
if self.timeout:
|
|
251
|
+
self.rc = self.proc.wait(timeout=self.timeout)
|
|
252
|
+
else:
|
|
253
|
+
self.rc = self.proc.wait()
|
|
254
|
+
except subprocess.TimeoutExpired:
|
|
255
|
+
print(f"Process timed out after {self.timeout}s")
|
|
256
|
+
self.proc.terminate()
|
|
257
|
+
try:
|
|
258
|
+
# Grace period for termination
|
|
259
|
+
self.rc = self.proc.wait(timeout=1)
|
|
260
|
+
except subprocess.TimeoutExpired:
|
|
261
|
+
self.proc.kill()
|
|
262
|
+
self.rc = self.proc.wait() # Final wait after kill
|
|
263
|
+
|
|
264
|
+
# Collect output after process has finished or been terminated
|
|
265
|
+
stdout, stderr = self.proc.communicate()
|
|
266
|
+
if stdout:
|
|
267
|
+
self.out_raw.extend(stdout.encode())
|
|
268
|
+
self.raw.extend(stdout.encode())
|
|
269
|
+
if not self.quiet:
|
|
270
|
+
print(stdout.rstrip())
|
|
271
|
+
if stderr:
|
|
272
|
+
self.err_raw.extend(stderr.encode())
|
|
273
|
+
self.raw.extend(stderr.encode())
|
|
274
|
+
if not self.quiet:
|
|
275
|
+
print(stderr.rstrip())
|
|
276
|
+
|
|
277
|
+
self.waited = True
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def stdout_ansi(self):
|
|
282
|
+
return self.out_raw.decode().rstrip()
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def stderr_ansi(self):
|
|
286
|
+
return self.err_raw.decode().rstrip()
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def out_ansi(self):
|
|
290
|
+
return self.raw.decode().rstrip()
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def stdout(self):
|
|
294
|
+
return ansi_escape.sub('', self.stdout_ansi)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def stderr(self):
|
|
298
|
+
return ansi_escape.sub('', self.stderr_ansi)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def out(self):
|
|
302
|
+
return ansi_escape.sub('', self.out_ansi)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.3
|
|
4
4
|
Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
|
|
5
5
|
Home-page: https://yourlabs.io/oss/cli2
|
|
6
6
|
Author: James Pic
|
|
@@ -63,6 +63,7 @@ Batteries included, all of which are useful on their own:
|
|
|
63
63
|
testing library so that you can go straight to the point in pytest
|
|
64
64
|
- a good old fcntl based locking
|
|
65
65
|
- a command line to run any python function over a beautiful CLI
|
|
66
|
-
- **AI
|
|
66
|
+
- **AI CLI with prompt2**
|
|
67
|
+
- **AI coding with code2** (TBA)
|
|
67
68
|
|
|
68
69
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|
|
@@ -10,12 +10,14 @@ cli2/colors.py
|
|
|
10
10
|
cli2/configuration.py
|
|
11
11
|
cli2/decorators.py
|
|
12
12
|
cli2/display.py
|
|
13
|
+
cli2/find.py
|
|
13
14
|
cli2/interactive.py
|
|
14
15
|
cli2/lock.py
|
|
15
16
|
cli2/log.py
|
|
16
17
|
cli2/mask.py
|
|
17
18
|
cli2/node.py
|
|
18
19
|
cli2/notlevenshtein.py
|
|
20
|
+
cli2/proc.py
|
|
19
21
|
cli2/sphinx.py
|
|
20
22
|
cli2/table.py
|
|
21
23
|
cli2/test.py
|
|
@@ -45,6 +47,7 @@ tests/test_configuration.py
|
|
|
45
47
|
tests/test_decorators.py
|
|
46
48
|
tests/test_display.py
|
|
47
49
|
tests/test_entry_point.py
|
|
50
|
+
tests/test_find.py
|
|
48
51
|
tests/test_group.py
|
|
49
52
|
tests/test_inject.py
|
|
50
53
|
tests/test_interactive.py
|
|
@@ -53,6 +56,7 @@ tests/test_log.py
|
|
|
53
56
|
tests/test_mask.py
|
|
54
57
|
tests/test_node.py
|
|
55
58
|
tests/test_notlevenshtein.py
|
|
59
|
+
tests/test_proc.py
|
|
56
60
|
tests/test_prompt2.py
|
|
57
61
|
tests/test_restful.py
|
|
58
62
|
tests/test_table.py
|
|
@@ -4,21 +4,6 @@ import pytest
|
|
|
4
4
|
from unittest import mock
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def test_async_run_noloop():
|
|
8
|
-
async def task():
|
|
9
|
-
await asyncio.sleep(.01)
|
|
10
|
-
|
|
11
|
-
cli2.async_run(task())
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.mark.asyncio
|
|
15
|
-
async def test_async_run_loop():
|
|
16
|
-
async def task():
|
|
17
|
-
await asyncio.sleep(.01)
|
|
18
|
-
|
|
19
|
-
cli2.async_run(task())
|
|
20
|
-
|
|
21
|
-
|
|
22
7
|
@pytest.mark.asyncio
|
|
23
8
|
async def test_queue_basic():
|
|
24
9
|
async def task(i):
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
from cli2 import Find
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def temp_git_repo(tmp_path):
|
|
8
|
+
"""Create a temporary git repository with test files."""
|
|
9
|
+
# Initialize git repo
|
|
10
|
+
os.system(f"git init {tmp_path}")
|
|
11
|
+
|
|
12
|
+
# Create some test files and directories
|
|
13
|
+
(tmp_path / "src").mkdir()
|
|
14
|
+
(tmp_path / "src" / "main.py").write_text("main content")
|
|
15
|
+
(tmp_path / "src" / "utils.py").write_text("utils content")
|
|
16
|
+
(tmp_path / "tests").mkdir()
|
|
17
|
+
(tmp_path / "tests" / "test_main.py").write_text("test content")
|
|
18
|
+
(tmp_path / ".gitignore").write_text("*.pyc\n__pycache__")
|
|
19
|
+
|
|
20
|
+
# Add files to git
|
|
21
|
+
os.system(f"cd {tmp_path} && git add . && git commit -m 'Initial commit'")
|
|
22
|
+
|
|
23
|
+
return tmp_path
|
|
24
|
+
|
|
25
|
+
def test_basic_initialization():
|
|
26
|
+
"""Test basic Find initialization."""
|
|
27
|
+
finder = Find()
|
|
28
|
+
assert isinstance(finder.root, Path)
|
|
29
|
+
assert finder.root.is_dir()
|
|
30
|
+
assert finder.glob_include == []
|
|
31
|
+
assert finder.glob_exclude == []
|
|
32
|
+
assert finder.file_callback is None
|
|
33
|
+
|
|
34
|
+
def test_custom_root_initialization(temp_git_repo):
|
|
35
|
+
"""Test initialization with custom root."""
|
|
36
|
+
finder = Find(root=temp_git_repo)
|
|
37
|
+
assert finder.root == temp_git_repo.resolve()
|
|
38
|
+
|
|
39
|
+
def test_files_listing(temp_git_repo):
|
|
40
|
+
"""Test basic file listing."""
|
|
41
|
+
finder = Find(root=temp_git_repo)
|
|
42
|
+
files = finder.files()
|
|
43
|
+
|
|
44
|
+
file_names = {f.name for f in files}
|
|
45
|
+
assert "main.py" in file_names
|
|
46
|
+
assert "utils.py" in file_names
|
|
47
|
+
assert "test_main.py" in file_names
|
|
48
|
+
assert ".gitignore" in file_names
|
|
49
|
+
|
|
50
|
+
def test_dirs_listing(temp_git_repo):
|
|
51
|
+
"""Test basic directory listing."""
|
|
52
|
+
finder = Find(root=temp_git_repo)
|
|
53
|
+
dirs = finder.dirs()
|
|
54
|
+
|
|
55
|
+
dir_names = {d.name for d in dirs}
|
|
56
|
+
assert "src" in dir_names
|
|
57
|
+
assert "tests" in dir_names
|
|
58
|
+
|
|
59
|
+
def test_glob_include_filter(temp_git_repo):
|
|
60
|
+
"""Test glob include filtering."""
|
|
61
|
+
finder = Find(root=temp_git_repo, glob_include=["*.py"])
|
|
62
|
+
files = finder.files()
|
|
63
|
+
|
|
64
|
+
file_names = {f.name for f in files}
|
|
65
|
+
assert "main.py" in file_names
|
|
66
|
+
assert "utils.py" in file_names
|
|
67
|
+
assert "test_main.py" in file_names
|
|
68
|
+
assert ".gitignore" not in file_names
|
|
69
|
+
|
|
70
|
+
def test_glob_exclude_filter(temp_git_repo):
|
|
71
|
+
"""Test glob exclude filtering."""
|
|
72
|
+
finder = Find(root=temp_git_repo, glob_exclude=["*test*"])
|
|
73
|
+
files = finder.files()
|
|
74
|
+
|
|
75
|
+
file_names = {f.name for f in files}
|
|
76
|
+
assert "main.py" in file_names
|
|
77
|
+
assert "utils.py" in file_names
|
|
78
|
+
assert "test_main.py" not in file_names
|
|
79
|
+
|
|
80
|
+
def test_file_callback(temp_git_repo):
|
|
81
|
+
"""Test file callback functionality."""
|
|
82
|
+
called_paths = []
|
|
83
|
+
def callback(filepath):
|
|
84
|
+
called_paths.append(filepath)
|
|
85
|
+
|
|
86
|
+
finder = Find(root=temp_git_repo, file_callback=callback)
|
|
87
|
+
files = finder.files()
|
|
88
|
+
|
|
89
|
+
assert len(called_paths) == len(files)
|
|
90
|
+
assert set(called_paths) == set(files)
|
|
91
|
+
|
|
92
|
+
def test_custom_directory_search(temp_git_repo):
|
|
93
|
+
"""Test searching from a specific directory."""
|
|
94
|
+
finder = Find(root=temp_git_repo)
|
|
95
|
+
files = finder.files(directory=temp_git_repo / "src")
|
|
96
|
+
|
|
97
|
+
file_names = {f.name for f in files}
|
|
98
|
+
assert "main.py" in file_names
|
|
99
|
+
assert "utils.py" in file_names
|
|
100
|
+
assert "test_main.py" not in file_names
|
|
101
|
+
|
|
102
|
+
def test_error_handling(temp_git_repo, monkeypatch):
|
|
103
|
+
"""Test error handling when git command fails."""
|
|
104
|
+
# Mock cli2.Proc to simulate git command failure
|
|
105
|
+
class MockProc:
|
|
106
|
+
def __init__(self, *args, **kwargs):
|
|
107
|
+
pass
|
|
108
|
+
def wait(self):
|
|
109
|
+
return self
|
|
110
|
+
rc = 1
|
|
111
|
+
stderr = "Git error"
|
|
112
|
+
stdout = ""
|
|
113
|
+
|
|
114
|
+
monkeypatch.setattr("cli2.Proc", MockProc)
|
|
115
|
+
|
|
116
|
+
finder = Find(root=temp_git_repo)
|
|
117
|
+
with pytest.raises(RuntimeError, match="Command failed with return code 1: Git error"):
|
|
118
|
+
finder.files()
|
|
119
|
+
|
|
120
|
+
def test_matches_filters(temp_git_repo):
|
|
121
|
+
"""Test the _matches_filters method."""
|
|
122
|
+
finder = Find(
|
|
123
|
+
root=temp_git_repo,
|
|
124
|
+
glob_include=["*.py"],
|
|
125
|
+
glob_exclude=["*test*"]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert finder._matches_filters(temp_git_repo / "src" / "main.py")
|
|
129
|
+
assert finder._matches_filters(temp_git_repo / "src" / "utils.py")
|
|
130
|
+
assert not finder._matches_filters(temp_git_repo / "tests" / "test_main.py")
|
|
131
|
+
assert not finder._matches_filters(temp_git_repo / ".gitignore")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import cli2
|
|
2
|
+
import os
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_proc_init_with_command_string():
|
|
8
|
+
proc = cli2.Proc("echo hello")
|
|
9
|
+
assert proc.args == ["echo", "hello"]
|
|
10
|
+
assert proc.quiet is False
|
|
11
|
+
assert proc.timeout is None
|
|
12
|
+
assert proc.env == os.environ
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_proc_init_with_command_and_args():
|
|
17
|
+
proc = cli2.Proc("echo", "hello", "world")
|
|
18
|
+
assert proc.args == ["echo", "hello", "world"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_proc_init_with_quiet():
|
|
23
|
+
proc = cli2.Proc("echo hello", quiet=True)
|
|
24
|
+
assert proc.quiet is True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_proc_init_with_timeout():
|
|
29
|
+
proc = cli2.Proc("echo hello", timeout=10)
|
|
30
|
+
assert proc.timeout == 10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_proc_init_with_env():
|
|
35
|
+
proc = cli2.Proc("echo hello", MY_VAR="test")
|
|
36
|
+
assert proc.env["MY_VAR"] == "test"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_proc_clone():
|
|
41
|
+
proc = cli2.Proc("echo hello", quiet=True, timeout=10, MY_VAR="test")
|
|
42
|
+
cloned_proc = proc.clone()
|
|
43
|
+
assert cloned_proc.args == proc.args
|
|
44
|
+
assert cloned_proc.quiet == proc.quiet
|
|
45
|
+
assert cloned_proc.timeout == proc.timeout
|
|
46
|
+
assert cloned_proc.env == proc.env
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_proc_cmd_property():
|
|
51
|
+
proc = cli2.Proc("echo hello")
|
|
52
|
+
assert proc.cmd == "echo hello"
|
|
53
|
+
proc.cmd = "echo world"
|
|
54
|
+
assert proc.args == ["echo", "world"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_proc_start_and_waita():
|
|
59
|
+
proc = cli2.Proc("echo hello")
|
|
60
|
+
await proc.starta()
|
|
61
|
+
await proc.waita()
|
|
62
|
+
assert proc.started is True
|
|
63
|
+
assert proc.waited is True
|
|
64
|
+
assert proc.rc == 0
|
|
65
|
+
assert proc.out == "hello"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_proc_start_and_wait_with_timeout():
|
|
70
|
+
proc = cli2.Proc("sleep 2", timeout=1)
|
|
71
|
+
await proc.starta()
|
|
72
|
+
await proc.waita()
|
|
73
|
+
assert proc.rc != 0 # Should be terminated due to timeout
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_proc_output_properties():
|
|
78
|
+
proc = cli2.Proc("echo hello")
|
|
79
|
+
await proc.starta()
|
|
80
|
+
await proc.waita()
|
|
81
|
+
assert proc.stdout == "hello"
|
|
82
|
+
assert proc.stderr == ""
|
|
83
|
+
assert proc.out == "hello"
|
|
84
|
+
assert proc.stdout_ansi == "hello"
|
|
85
|
+
assert proc.stderr_ansi == ""
|
|
86
|
+
assert proc.out_ansi == "hello"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_proc_with_stderr():
|
|
91
|
+
proc = cli2.Proc("bash", "-c", "echo error >&2 ")
|
|
92
|
+
await proc.starta()
|
|
93
|
+
await proc.waita()
|
|
94
|
+
assert proc.stdout == ""
|
|
95
|
+
assert proc.stderr == "error"
|
|
96
|
+
assert proc.out == "error"
|
|
97
|
+
assert proc.stdout_ansi == ""
|
|
98
|
+
assert proc.stderr_ansi == "error"
|
|
99
|
+
assert proc.out_ansi == "error"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_proc_with_ansi_codes():
|
|
104
|
+
proc = cli2.Proc("echo -e '\033[31mred\033[0m'")
|
|
105
|
+
await proc.starta()
|
|
106
|
+
await proc.waita()
|
|
107
|
+
assert proc.stdout == "red"
|
|
108
|
+
assert proc.stdout_ansi == "\x1b[31mred\x1b[0m"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_proc_quiet_mode():
|
|
113
|
+
proc = cli2.Proc("echo hello", quiet=True)
|
|
114
|
+
await proc.starta()
|
|
115
|
+
await proc.waita()
|
|
116
|
+
assert proc.out == "hello"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_proc_start_and_wait():
|
|
120
|
+
proc = cli2.Proc("echo hello").wait()
|
|
121
|
+
assert proc.started is True
|
|
122
|
+
assert proc.waited is True
|
|
123
|
+
assert proc.rc == 0
|
|
124
|
+
assert proc.out == "hello"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_proc_start_and_wait_with_timeout():
|
|
128
|
+
proc = cli2.Proc("sleep 2", timeout=1).wait()
|
|
129
|
+
assert proc.rc != 0 # Should be terminated due to timeout
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_proc_start_and_wait_with_stderr():
|
|
133
|
+
proc = cli2.Proc("bash", "-c", "echo error >&2").wait()
|
|
134
|
+
assert proc.stdout == ""
|
|
135
|
+
assert proc.stderr == "error"
|
|
136
|
+
assert proc.out == "error"
|
|
137
|
+
assert proc.stdout_ansi == ""
|
|
138
|
+
assert proc.stderr_ansi == "error"
|
|
139
|
+
assert proc.out_ansi == "error"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_proc_start_and_wait_with_ansi_codes():
|
|
143
|
+
proc = cli2.Proc("echo -e '\033[31mred\033[0m'").wait()
|
|
144
|
+
assert proc.stdout == "red"
|
|
145
|
+
assert proc.stdout_ansi == "\x1b[31mred\x1b[0m"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_proc_start_and_wait_quiet_mode():
|
|
149
|
+
proc = cli2.Proc("echo hello", quiet=True).wait()
|
|
150
|
+
assert proc.out == "hello"
|
|
@@ -7,21 +7,52 @@ from unittest import mock
|
|
|
7
7
|
from prompt2 import cli, Model, Prompt
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def test_model():
|
|
11
|
+
os.environ['MODEL'] = 'litellm foo bar=1 foo=.2'
|
|
12
|
+
model = Model()
|
|
13
|
+
assert type(model.backend).__name__ == 'LiteLLMBackend'
|
|
14
|
+
assert model.backend.model_name == 'foo'
|
|
15
|
+
assert model.backend.model_kwargs['bar'] == 1
|
|
16
|
+
assert model.backend.model_kwargs['foo'] == .2
|
|
17
|
+
del os.environ['MODEL']
|
|
18
|
+
|
|
19
|
+
os.environ['MODEL_FOO'] = 'test a=b'
|
|
20
|
+
model = Model('foo')
|
|
21
|
+
assert type(model.backend).__name__ == 'LiteLLMBackend'
|
|
22
|
+
assert model.backend.model_name == 'test'
|
|
23
|
+
assert model.backend.model_kwargs['a'] == 'b'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_prompt(user, local):
|
|
27
|
+
with user.open('w') as f:
|
|
28
|
+
f.write('hello')
|
|
29
|
+
|
|
30
|
+
args = ['user', str(user), user]
|
|
31
|
+
for arg in args:
|
|
32
|
+
prompt = Prompt(arg)
|
|
33
|
+
assert prompt.path == user, arg
|
|
34
|
+
assert prompt.content == 'hello', arg
|
|
35
|
+
assert prompt.name == 'user'
|
|
36
|
+
|
|
37
|
+
|
|
10
38
|
def test_paths():
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
]
|
|
39
|
+
paths = cli.cli('paths')
|
|
40
|
+
assert paths[0] == str(Prompt.local_path)
|
|
41
|
+
assert paths[1] == str(Prompt.user_path)
|
|
15
42
|
|
|
16
43
|
|
|
17
44
|
@pytest.fixture
|
|
18
|
-
def user():
|
|
19
|
-
|
|
45
|
+
def user(prompt2_env):
|
|
46
|
+
path = Path(prompt2_env.get('PROMPT2_USER_PATH'))
|
|
47
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
return path / 'user.txt'
|
|
20
49
|
|
|
21
50
|
|
|
22
51
|
@pytest.fixture
|
|
23
|
-
def local():
|
|
24
|
-
|
|
52
|
+
def local(prompt2_env):
|
|
53
|
+
path = Path(prompt2_env.get('PROMPT2_LOCAL_PATH'))
|
|
54
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
return path / 'local.txt'
|
|
25
56
|
|
|
26
57
|
|
|
27
58
|
@pytest.fixture
|
|
@@ -39,10 +70,8 @@ def kwargs(prompt2_env, user, local):
|
|
|
39
70
|
|
|
40
71
|
@pytest.mark.asyncio
|
|
41
72
|
async def test_python(prompt2_env):
|
|
42
|
-
model = Model
|
|
43
|
-
|
|
44
|
-
prompt = Prompt()
|
|
45
|
-
prompt.parts.append('make a hello world in python')
|
|
73
|
+
model = Model()
|
|
74
|
+
prompt = Prompt(content='make a hello world in python')
|
|
46
75
|
result = await model(prompt)
|
|
47
76
|
assert 'To run this:' in result
|
|
48
77
|
result = await model(prompt, 'wholefile')
|
|
@@ -67,6 +96,32 @@ def test_parsers(kwargs):
|
|
|
67
96
|
)
|
|
68
97
|
|
|
69
98
|
|
|
99
|
+
def test_ask(kwargs):
|
|
100
|
+
autotest(
|
|
101
|
+
'tests/prompt2/test_ask.txt',
|
|
102
|
+
'prompt2 ask Write hello world in python',
|
|
103
|
+
**kwargs,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_command():
|
|
108
|
+
from prompt2.cli import PromptCommand
|
|
109
|
+
def test(model=None, parser=None):
|
|
110
|
+
return model, parser
|
|
111
|
+
model, parser = PromptCommand(test)()
|
|
112
|
+
assert type(model.backend).__name__ == 'LiteLLMBackend'
|
|
113
|
+
assert model.backend.model_name == Model.default
|
|
114
|
+
assert not parser
|
|
115
|
+
|
|
116
|
+
os.environ['MODEL_LOL'] = 'foo temperature=test'
|
|
117
|
+
model, parser = PromptCommand(test)('lol')
|
|
118
|
+
assert model.backend.model_name == 'foo'
|
|
119
|
+
assert model.backend.model_kwargs == dict(temperature='test')
|
|
120
|
+
|
|
121
|
+
model, parser = PromptCommand(test)('parser=wholefile')
|
|
122
|
+
assert type(parser).__name__.lower() == 'wholefile'
|
|
123
|
+
|
|
124
|
+
|
|
70
125
|
def test_crud(user, kwargs):
|
|
71
126
|
autotest(
|
|
72
127
|
'tests/prompt2/test_edit_user.txt',
|
|
@@ -142,3 +197,14 @@ def test_crud(user, kwargs):
|
|
|
142
197
|
'prompt2 send user wholefile',
|
|
143
198
|
**kwargs,
|
|
144
199
|
)
|
|
200
|
+
autotest(
|
|
201
|
+
'tests/prompt2/test_send_code_failparser.txt',
|
|
202
|
+
'prompt2 send user foeuau',
|
|
203
|
+
**kwargs,
|
|
204
|
+
)
|
|
205
|
+
os.environ['MODEL_FOO'] = 'test a=b'
|
|
206
|
+
autotest(
|
|
207
|
+
'tests/prompt2/test_send_code_failmodel.txt',
|
|
208
|
+
'prompt2 send user model=oaeoeau',
|
|
209
|
+
**kwargs,
|
|
210
|
+
)
|
|
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
|
|
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
|