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.
Files changed (62) hide show
  1. {cli2-5.2.1/cli2.egg-info → cli2-5.2.2}/PKG-INFO +3 -2
  2. {cli2-5.2.1 → cli2-5.2.2}/README.rst +2 -1
  3. {cli2-5.2.1 → cli2-5.2.2}/cli2/__init__.py +2 -1
  4. {cli2-5.2.1 → cli2-5.2.2}/cli2/asyncio.py +0 -17
  5. {cli2-5.2.1 → cli2-5.2.2}/cli2/interactive.py +32 -16
  6. cli2-5.2.2/cli2/proc.py +303 -0
  7. {cli2-5.2.1 → cli2-5.2.2/cli2.egg-info}/PKG-INFO +3 -2
  8. {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/SOURCES.txt +2 -0
  9. {cli2-5.2.1 → cli2-5.2.2}/setup.py +1 -1
  10. {cli2-5.2.1 → cli2-5.2.2}/tests/test_asyncio.py +0 -15
  11. cli2-5.2.2/tests/test_proc.py +150 -0
  12. {cli2-5.2.1 → cli2-5.2.2}/tests/test_prompt2.py +78 -12
  13. {cli2-5.2.1 → cli2-5.2.2}/MANIFEST.in +0 -0
  14. {cli2-5.2.1 → cli2-5.2.2}/classifiers.txt +0 -0
  15. {cli2-5.2.1 → cli2-5.2.2}/cli2/cli.py +0 -0
  16. {cli2-5.2.1 → cli2-5.2.2}/cli2/cli2.py +0 -0
  17. {cli2-5.2.1 → cli2-5.2.2}/cli2/colors.py +0 -0
  18. {cli2-5.2.1 → cli2-5.2.2}/cli2/configuration.py +0 -0
  19. {cli2-5.2.1 → cli2-5.2.2}/cli2/decorators.py +0 -0
  20. {cli2-5.2.1 → cli2-5.2.2}/cli2/display.py +0 -0
  21. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/__init__.py +0 -0
  22. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/conf.py +0 -0
  23. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/example.py +0 -0
  24. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/example_obj.py +0 -0
  25. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/nesting.py +0 -0
  26. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/obj.py +0 -0
  27. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/obj2.py +0 -0
  28. {cli2-5.2.1 → cli2-5.2.2}/cli2/examples/test.py +0 -0
  29. {cli2-5.2.1 → cli2-5.2.2}/cli2/lock.py +0 -0
  30. {cli2-5.2.1 → cli2-5.2.2}/cli2/log.py +0 -0
  31. {cli2-5.2.1 → cli2-5.2.2}/cli2/mask.py +0 -0
  32. {cli2-5.2.1 → cli2-5.2.2}/cli2/node.py +0 -0
  33. {cli2-5.2.1 → cli2-5.2.2}/cli2/notlevenshtein.py +0 -0
  34. {cli2-5.2.1 → cli2-5.2.2}/cli2/sphinx.py +0 -0
  35. {cli2-5.2.1 → cli2-5.2.2}/cli2/table.py +0 -0
  36. {cli2-5.2.1 → cli2-5.2.2}/cli2/test.py +0 -0
  37. {cli2-5.2.1 → cli2-5.2.2}/cli2/theme.py +0 -0
  38. {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/dependency_links.txt +0 -0
  39. {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/entry_points.txt +0 -0
  40. {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/requires.txt +0 -0
  41. {cli2-5.2.1 → cli2-5.2.2}/cli2.egg-info/top_level.txt +0 -0
  42. {cli2-5.2.1 → cli2-5.2.2}/setup.cfg +0 -0
  43. {cli2-5.2.1 → cli2-5.2.2}/tests/test_ansible.py +0 -0
  44. {cli2-5.2.1 → cli2-5.2.2}/tests/test_ansible_variables.py +0 -0
  45. {cli2-5.2.1 → cli2-5.2.2}/tests/test_cli.py +0 -0
  46. {cli2-5.2.1 → cli2-5.2.2}/tests/test_client.py +0 -0
  47. {cli2-5.2.1 → cli2-5.2.2}/tests/test_client_test.py +0 -0
  48. {cli2-5.2.1 → cli2-5.2.2}/tests/test_command.py +0 -0
  49. {cli2-5.2.1 → cli2-5.2.2}/tests/test_configuration.py +0 -0
  50. {cli2-5.2.1 → cli2-5.2.2}/tests/test_decorators.py +0 -0
  51. {cli2-5.2.1 → cli2-5.2.2}/tests/test_display.py +0 -0
  52. {cli2-5.2.1 → cli2-5.2.2}/tests/test_entry_point.py +0 -0
  53. {cli2-5.2.1 → cli2-5.2.2}/tests/test_group.py +0 -0
  54. {cli2-5.2.1 → cli2-5.2.2}/tests/test_inject.py +0 -0
  55. {cli2-5.2.1 → cli2-5.2.2}/tests/test_interactive.py +0 -0
  56. {cli2-5.2.1 → cli2-5.2.2}/tests/test_lock.py +0 -0
  57. {cli2-5.2.1 → cli2-5.2.2}/tests/test_log.py +0 -0
  58. {cli2-5.2.1 → cli2-5.2.2}/tests/test_mask.py +0 -0
  59. {cli2-5.2.1 → cli2-5.2.2}/tests/test_node.py +0 -0
  60. {cli2-5.2.1 → cli2-5.2.2}/tests/test_notlevenshtein.py +0 -0
  61. {cli2-5.2.1 → cli2-5.2.2}/tests/test_restful.py +0 -0
  62. {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.1
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 assisted programing CLI & framework with code2**
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 assisted programing CLI & framework with code2**
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, async_run, Queue
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
- :param content: Initial content if any
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
- try:
78
- os.remove(filepath)
79
- except OSError as e:
80
- log.warn(f"Error deleting temporary file {filepath}: {e}")
92
+ if tmp:
93
+ try:
94
+ os.remove(filepath)
95
+ except OSError as e:
96
+ log.warn(f"Error deleting temporary file {filepath}: {e}")
@@ -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.1
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 assisted programing CLI & framework with code2**
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
@@ -44,7 +44,7 @@ from setuptools import setup
44
44
 
45
45
  setup(
46
46
  name='cli2',
47
- version='5.2.1',
47
+ version='5.2.2',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -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
- assert cli.cli('paths') == [
12
- str(Prompt.LOCAL_PATH),
13
- str(Prompt.USER_PATH),
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
- return Path(os.getenv('PROMPT2_USER_PATH')) / 'user.txt'
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
- return Path(os.getenv('PROMPT2_LOCAL_PATH')) / 'local.txt'
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.get()
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