cli2 5.2.1__tar.gz → 5.2.2__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.1/cli2.egg-info → cli2-5.2.2}/PKG-INFO +3 -2
- {cli2-5.2.1 → cli2-5.2.2}/README.rst +2 -1
- {cli2-5.2.1 → cli2-5.2.2}/cli2/__init__.py +2 -1
- {cli2-5.2.1 → cli2-5.2.2}/cli2/asyncio.py +0 -17
- {cli2-5.2.1 → cli2-5.2.2}/cli2/interactive.py +32 -16
- cli2-5.2.2/cli2/proc.py +303 -0
- {cli2-5.2.1 → cli2-5.2.2/cli2.egg-info}/PKG-INFO +3 -2
- {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/SOURCES.txt +2 -0
- {cli2-5.2.1 → cli2-5.2.2}/setup.py +1 -1
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_asyncio.py +0 -15
- cli2-5.2.2/tests/test_proc.py +150 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_prompt2.py +78 -12
- {cli2-5.2.1 → cli2-5.2.2}/MANIFEST.in +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/classifiers.txt +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/cli.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/cli2.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/colors.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/configuration.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/decorators.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/display.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/__init__.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/conf.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/example.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/example_obj.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/nesting.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/obj.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/obj2.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/test.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/lock.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/log.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/mask.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/node.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/notlevenshtein.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/sphinx.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/table.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/test.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2/theme.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/requires.txt +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/top_level.txt +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/setup.cfg +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_ansible.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_ansible_variables.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_cli.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_client.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_client_test.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_command.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_configuration.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_decorators.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_display.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_entry_point.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_group.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_inject.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_interactive.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_lock.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_log.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_mask.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_node.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_notlevenshtein.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/tests/test_restful.py +0 -0
- {cli2-5.2.1 → cli2-5.2.2}/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.2
|
|
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,7 @@ 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
|
|
31
32
|
from .table import Table
|
|
32
33
|
|
|
33
34
|
|
|
@@ -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.
|
|
@@ -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.2/cli2/proc.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
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, butter when building commands
|
|
18
|
+
proc = cli2.Proc('foo', 'bar')
|
|
19
|
+
|
|
20
|
+
# run in sync mode (ie. for jinja2)
|
|
21
|
+
proc.wait_sync()
|
|
22
|
+
|
|
23
|
+
# OR run in async loop
|
|
24
|
+
await proc.wait()
|
|
25
|
+
|
|
26
|
+
# You can chain
|
|
27
|
+
proc = cli2.Proc('hi').wait_sync()
|
|
28
|
+
proc = await cli2.Proc('hi').wait()
|
|
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
|
+
"""
|
|
34
|
+
import asyncio
|
|
35
|
+
import os
|
|
36
|
+
import shlex
|
|
37
|
+
import re
|
|
38
|
+
import subprocess
|
|
39
|
+
|
|
40
|
+
from .log import log
|
|
41
|
+
|
|
42
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Proc:
|
|
46
|
+
"""
|
|
47
|
+
Asynchronous subprocess manager with advanced IO handling.
|
|
48
|
+
|
|
49
|
+
.. py:attribute:: args
|
|
50
|
+
|
|
51
|
+
Full command arguments list used to launch the process
|
|
52
|
+
|
|
53
|
+
.. py:attribute:: rc
|
|
54
|
+
|
|
55
|
+
Return Code: process exit code (available after process completes)
|
|
56
|
+
|
|
57
|
+
.. py:attribute:: out
|
|
58
|
+
|
|
59
|
+
Combined cleaned output with ANSI escape codes removed.
|
|
60
|
+
|
|
61
|
+
.. py:attribute:: out_ansi
|
|
62
|
+
|
|
63
|
+
Combined stdout/stderr output with ANSI codes preserved.
|
|
64
|
+
|
|
65
|
+
.. py:attribute:: stdout
|
|
66
|
+
|
|
67
|
+
Cleaned stdout output with ANSI escape codes removed.
|
|
68
|
+
|
|
69
|
+
.. py:attribute:: stderr
|
|
70
|
+
|
|
71
|
+
Cleaned stdout output with ANSI escape codes removed.
|
|
72
|
+
|
|
73
|
+
.. py:attribute:: stdout_ansi
|
|
74
|
+
|
|
75
|
+
Stdout output with ANSI escape codes preserved.
|
|
76
|
+
|
|
77
|
+
.. py:attribute:: stderr_ansi
|
|
78
|
+
|
|
79
|
+
Stderr output with ANSI escape codes preserved.
|
|
80
|
+
"""
|
|
81
|
+
def __init__(self, cmd, *args, quiet=False, inherit=True, timeout=None,
|
|
82
|
+
**env):
|
|
83
|
+
"""
|
|
84
|
+
:param cmd: Command string (will shlex split) or initial argument
|
|
85
|
+
:param args: Additional command arguments
|
|
86
|
+
:param quiet: Suppress live output printing (default: False)
|
|
87
|
+
:param inherit: Inherit parent environment variables (default: True)
|
|
88
|
+
:param timeout: Maximum execution time in seconds (default: None)
|
|
89
|
+
:param env: Additional environment variables to set
|
|
90
|
+
:type env: Environment variables.
|
|
91
|
+
"""
|
|
92
|
+
if args:
|
|
93
|
+
self.args = [cmd] + list(args)
|
|
94
|
+
else:
|
|
95
|
+
self.args = shlex.split(cmd)
|
|
96
|
+
|
|
97
|
+
self.quiet = quiet
|
|
98
|
+
|
|
99
|
+
self.env = dict()
|
|
100
|
+
if inherit:
|
|
101
|
+
self.env = os.environ.copy()
|
|
102
|
+
self.env.update(env)
|
|
103
|
+
|
|
104
|
+
self.out_raw = bytearray()
|
|
105
|
+
self.err_raw = bytearray()
|
|
106
|
+
self.raw = bytearray()
|
|
107
|
+
|
|
108
|
+
self.started = False
|
|
109
|
+
self.waited = False
|
|
110
|
+
self.timeout = timeout
|
|
111
|
+
self.rc = None
|
|
112
|
+
self.proc = None
|
|
113
|
+
|
|
114
|
+
def clone(self):
|
|
115
|
+
"""
|
|
116
|
+
Create a new unstarted Proc instance with identical configuration.
|
|
117
|
+
|
|
118
|
+
:return: New Proc instance ready for execution
|
|
119
|
+
"""
|
|
120
|
+
return type(self)(
|
|
121
|
+
*self.args, quiet=self.quiet, inherit=True, timeout=self.timeout,
|
|
122
|
+
**self.env
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def cmd(self):
|
|
127
|
+
"""
|
|
128
|
+
Get/set the command as a shell-joinable string.
|
|
129
|
+
|
|
130
|
+
:getter: Returns shell-escaped command string
|
|
131
|
+
:setter: Parses and updates internal args list
|
|
132
|
+
:type: str
|
|
133
|
+
"""
|
|
134
|
+
return shlex.join(self.args)
|
|
135
|
+
|
|
136
|
+
@cmd.setter
|
|
137
|
+
def cmd(self, value):
|
|
138
|
+
self.args = shlex.split(value)
|
|
139
|
+
|
|
140
|
+
async def start(self):
|
|
141
|
+
"""
|
|
142
|
+
Launch the subprocess asynchronously.
|
|
143
|
+
|
|
144
|
+
:return: Self reference for method chaining
|
|
145
|
+
:raises RuntimeError: If process is already started
|
|
146
|
+
"""
|
|
147
|
+
if self.started:
|
|
148
|
+
raise RuntimeError("Process already started")
|
|
149
|
+
|
|
150
|
+
if not self.quiet:
|
|
151
|
+
log.debug('cmd', cmd=self.cmd)
|
|
152
|
+
|
|
153
|
+
self.proc = await asyncio.create_subprocess_exec(
|
|
154
|
+
*[str(arg) for arg in self.args],
|
|
155
|
+
stdin=asyncio.subprocess.PIPE,
|
|
156
|
+
stdout=asyncio.subprocess.PIPE,
|
|
157
|
+
stderr=asyncio.subprocess.PIPE,
|
|
158
|
+
env={str(k): str(v) for k, v in self.env.items()},
|
|
159
|
+
)
|
|
160
|
+
self.started = True
|
|
161
|
+
|
|
162
|
+
self.stdout_task = asyncio.create_task(
|
|
163
|
+
self._handle_output(self.proc.stdout, 1)
|
|
164
|
+
)
|
|
165
|
+
self.stderr_task = asyncio.create_task(
|
|
166
|
+
self._handle_output(self.proc.stderr, 2)
|
|
167
|
+
)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
async def wait(self):
|
|
171
|
+
"""
|
|
172
|
+
Wait for process completion with timeout handling.
|
|
173
|
+
|
|
174
|
+
Terminates process if timeout occurs. Gathers all output streams.
|
|
175
|
+
|
|
176
|
+
:return: Self reference for method chaining
|
|
177
|
+
"""
|
|
178
|
+
if not self.started:
|
|
179
|
+
await self.start()
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if self.timeout:
|
|
183
|
+
await asyncio.wait_for(self.proc.wait(), timeout=self.timeout)
|
|
184
|
+
else:
|
|
185
|
+
await self.proc.wait()
|
|
186
|
+
except asyncio.TimeoutError:
|
|
187
|
+
print(f"Process timed out after {self.timeout}s")
|
|
188
|
+
self.proc.terminate()
|
|
189
|
+
await self.proc.wait()
|
|
190
|
+
|
|
191
|
+
await asyncio.gather(self.stdout_task, self.stderr_task)
|
|
192
|
+
self.rc = self.proc.returncode
|
|
193
|
+
self.waited = True
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
async def _handle_output(self, stream, fd):
|
|
197
|
+
"""
|
|
198
|
+
Internal method for stream handling.
|
|
199
|
+
|
|
200
|
+
:param stream: Output stream to monitor
|
|
201
|
+
:type stream: asyncio.StreamReader
|
|
202
|
+
:param fd: Stream identifier (1=stdout, 2=stderr)
|
|
203
|
+
:type fd: int
|
|
204
|
+
"""
|
|
205
|
+
while True:
|
|
206
|
+
line = await stream.readline()
|
|
207
|
+
if not line: # EOF
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
decoded_line = line.decode().rstrip()
|
|
211
|
+
if fd == 1: # stdout
|
|
212
|
+
self.out_raw.extend(line)
|
|
213
|
+
elif fd == 2: # stderr
|
|
214
|
+
self.err_raw.extend(line)
|
|
215
|
+
self.raw.extend(line)
|
|
216
|
+
|
|
217
|
+
if not self.quiet:
|
|
218
|
+
print(decoded_line)
|
|
219
|
+
|
|
220
|
+
def start_sync(self):
|
|
221
|
+
"""
|
|
222
|
+
Start the subprocess synchronously without waiting for output.
|
|
223
|
+
"""
|
|
224
|
+
if self.started:
|
|
225
|
+
raise RuntimeError("Process already started")
|
|
226
|
+
|
|
227
|
+
if not self.quiet:
|
|
228
|
+
log.debug('cmd', cmd=self.cmd)
|
|
229
|
+
|
|
230
|
+
self.proc = subprocess.Popen(
|
|
231
|
+
self.args,
|
|
232
|
+
stdin=subprocess.PIPE,
|
|
233
|
+
stdout=subprocess.PIPE,
|
|
234
|
+
stderr=subprocess.PIPE,
|
|
235
|
+
env=self.env,
|
|
236
|
+
universal_newlines=True
|
|
237
|
+
)
|
|
238
|
+
self.started = True
|
|
239
|
+
# Do NOT start output handling here; defer to wait_sync
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def wait_sync(self):
|
|
243
|
+
"""
|
|
244
|
+
Wait for process completion synchronously with timeout handling.
|
|
245
|
+
Collects output streams after waiting.
|
|
246
|
+
"""
|
|
247
|
+
if not self.started:
|
|
248
|
+
self.start_sync()
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
if self.timeout:
|
|
252
|
+
self.rc = self.proc.wait(timeout=self.timeout)
|
|
253
|
+
else:
|
|
254
|
+
self.rc = self.proc.wait()
|
|
255
|
+
except subprocess.TimeoutExpired:
|
|
256
|
+
print(f"Process timed out after {self.timeout}s")
|
|
257
|
+
self.proc.terminate()
|
|
258
|
+
try:
|
|
259
|
+
# Grace period for termination
|
|
260
|
+
self.rc = self.proc.wait(timeout=1)
|
|
261
|
+
except subprocess.TimeoutExpired:
|
|
262
|
+
self.proc.kill()
|
|
263
|
+
self.rc = self.proc.wait() # Final wait after kill
|
|
264
|
+
|
|
265
|
+
# Collect output after process has finished or been terminated
|
|
266
|
+
stdout, stderr = self.proc.communicate()
|
|
267
|
+
if stdout:
|
|
268
|
+
self.out_raw.extend(stdout.encode())
|
|
269
|
+
self.raw.extend(stdout.encode())
|
|
270
|
+
if not self.quiet:
|
|
271
|
+
print(stdout.rstrip())
|
|
272
|
+
if stderr:
|
|
273
|
+
self.err_raw.extend(stderr.encode())
|
|
274
|
+
self.raw.extend(stderr.encode())
|
|
275
|
+
if not self.quiet:
|
|
276
|
+
print(stderr.rstrip())
|
|
277
|
+
|
|
278
|
+
self.waited = True
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def stdout_ansi(self):
|
|
283
|
+
return self.out_raw.decode().rstrip()
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def stderr_ansi(self):
|
|
287
|
+
return self.err_raw.decode().rstrip()
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def out_ansi(self):
|
|
291
|
+
return self.raw.decode().rstrip()
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def stdout(self):
|
|
295
|
+
return ansi_escape.sub('', self.stdout_ansi)
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def stderr(self):
|
|
299
|
+
return ansi_escape.sub('', self.stderr_ansi)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def out(self):
|
|
303
|
+
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.2
|
|
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>`_.
|
|
@@ -16,6 +16,7 @@ cli2/log.py
|
|
|
16
16
|
cli2/mask.py
|
|
17
17
|
cli2/node.py
|
|
18
18
|
cli2/notlevenshtein.py
|
|
19
|
+
cli2/proc.py
|
|
19
20
|
cli2/sphinx.py
|
|
20
21
|
cli2/table.py
|
|
21
22
|
cli2/test.py
|
|
@@ -53,6 +54,7 @@ tests/test_log.py
|
|
|
53
54
|
tests/test_mask.py
|
|
54
55
|
tests/test_node.py
|
|
55
56
|
tests/test_notlevenshtein.py
|
|
57
|
+
tests/test_proc.py
|
|
56
58
|
tests/test_prompt2.py
|
|
57
59
|
tests/test_restful.py
|
|
58
60
|
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,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_wait():
|
|
59
|
+
proc = cli2.Proc("echo hello")
|
|
60
|
+
await proc.start()
|
|
61
|
+
await proc.wait()
|
|
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.start()
|
|
72
|
+
await proc.wait()
|
|
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.start()
|
|
80
|
+
await proc.wait()
|
|
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.start()
|
|
93
|
+
await proc.wait()
|
|
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.start()
|
|
106
|
+
await proc.wait()
|
|
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.start()
|
|
115
|
+
await proc.wait()
|
|
116
|
+
assert proc.out == "hello"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_proc_start_sync_and_wait_sync():
|
|
120
|
+
proc = cli2.Proc("echo hello").wait_sync()
|
|
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_sync_and_wait_sync_with_timeout():
|
|
128
|
+
proc = cli2.Proc("sleep 2", timeout=1).wait_sync()
|
|
129
|
+
assert proc.rc != 0 # Should be terminated due to timeout
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_proc_start_sync_and_wait_sync_with_stderr():
|
|
133
|
+
proc = cli2.Proc("bash", "-c", "echo error >&2").wait_sync()
|
|
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_sync_and_wait_sync_with_ansi_codes():
|
|
143
|
+
proc = cli2.Proc("echo -e '\033[31mred\033[0m'").wait_sync()
|
|
144
|
+
assert proc.stdout == "red"
|
|
145
|
+
assert proc.stdout_ansi == "\x1b[31mred\x1b[0m"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_proc_start_sync_and_wait_sync_quiet_mode():
|
|
149
|
+
proc = cli2.Proc("echo hello", quiet=True).wait_sync()
|
|
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
|