cli2 5.1.6__tar.gz → 5.2.1rc1__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.1.6/cli2.egg-info → cli2-5.2.1rc1}/PKG-INFO +2 -1
- {cli2-5.1.6 → cli2-5.2.1rc1}/README.rst +1 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/__init__.py +6 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/cli.py +36 -8
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/configuration.py +17 -1
- cli2-5.2.1rc1/cli2/interactive.py +80 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/log.py +17 -10
- cli2-5.2.1rc1/cli2/notlevenshtein.py +74 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/table.py +1 -1
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/test.py +5 -5
- cli2-5.2.1rc1/cli2/theme.py +223 -0
- {cli2-5.1.6 → cli2-5.2.1rc1/cli2.egg-info}/PKG-INFO +2 -1
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2.egg-info/SOURCES.txt +6 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2.egg-info/entry_points.txt +2 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/setup.py +3 -1
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_ansible.py +1 -1
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_cli.py +3 -3
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_client.py +6 -5
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_command.py +7 -6
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_configuration.py +3 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_group.py +14 -6
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_inject.py +1 -1
- cli2-5.2.1rc1/tests/test_interactive.py +50 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_lock.py +1 -1
- cli2-5.2.1rc1/tests/test_notlevenshtein.py +46 -0
- cli2-5.2.1rc1/tests/test_prompt2.py +144 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/MANIFEST.in +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/classifiers.txt +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/asyncio.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/cli2.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/colors.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/decorators.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/display.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/__init__.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/conf.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/example.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/example_obj.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/nesting.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/obj.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/obj2.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/examples/test.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/lock.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/mask.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/node.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2/sphinx.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2.egg-info/requires.txt +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/cli2.egg-info/top_level.txt +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/setup.cfg +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_ansible_variables.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_asyncio.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_client_test.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_decorators.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_display.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_entry_point.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_log.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_mask.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_node.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_restful.py +0 -0
- {cli2-5.1.6 → cli2-5.2.1rc1}/tests/test_table.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.1rc1
|
|
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,5 +63,6 @@ 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
67
|
|
|
67
68
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|
|
@@ -25,5 +25,6 @@ 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
29
|
|
|
29
30
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|
|
@@ -8,20 +8,26 @@ from .cli import (
|
|
|
8
8
|
Command,
|
|
9
9
|
Group,
|
|
10
10
|
EntryPoint,
|
|
11
|
+
Cli2Error,
|
|
12
|
+
Cli2ValueError,
|
|
11
13
|
)
|
|
12
14
|
from .asyncio import async_resolve, async_run, Queue
|
|
13
15
|
from .colors import colors as c
|
|
16
|
+
from .theme import theme, t
|
|
14
17
|
|
|
15
18
|
from .configuration import Configuration, cfg
|
|
16
19
|
from .display import diff, diff_data, render, print, highlight, yaml_highlight
|
|
20
|
+
from .interactive import choice, editor
|
|
17
21
|
try:
|
|
18
22
|
import fcntl
|
|
19
23
|
except ImportError:
|
|
20
24
|
""" windows """
|
|
21
25
|
else:
|
|
22
26
|
from .lock import Lock
|
|
27
|
+
|
|
23
28
|
from .log import configure, log, parse
|
|
24
29
|
from .mask import Mask
|
|
30
|
+
from .notlevenshtein import closest, closest_path
|
|
25
31
|
from .table import Table
|
|
26
32
|
|
|
27
33
|
|
|
@@ -14,6 +14,14 @@ from .asyncio import async_resolve
|
|
|
14
14
|
from .colors import colors
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class Cli2Error(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Cli2ValueError(Cli2Error):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
class Overrides(dict):
|
|
18
26
|
"""
|
|
19
27
|
Lazy overrides dict
|
|
@@ -299,6 +307,9 @@ class Group(EntryPoint, dict):
|
|
|
299
307
|
continue
|
|
300
308
|
if leaf and getattr(final, name, '_') is None:
|
|
301
309
|
continue
|
|
310
|
+
if isinstance(method, classmethod):
|
|
311
|
+
# get the bound method
|
|
312
|
+
method = getattr(cls, name)
|
|
302
313
|
self.load_method(final, method)
|
|
303
314
|
|
|
304
315
|
def load_obj(self, obj):
|
|
@@ -308,9 +319,10 @@ class Group(EntryPoint, dict):
|
|
|
308
319
|
for name in dir(obj):
|
|
309
320
|
if name.startswith('_'):
|
|
310
321
|
continue
|
|
311
|
-
if not callable(getattr(type(obj), name)):
|
|
322
|
+
if not callable(getattr(type(obj), name, None)):
|
|
312
323
|
continue
|
|
313
|
-
|
|
324
|
+
method = getattr(obj, name)
|
|
325
|
+
self.load_method(obj, method)
|
|
314
326
|
|
|
315
327
|
def load_method(self, obj, method):
|
|
316
328
|
wrapped_method = getattr(method, '__func__', None)
|
|
@@ -325,7 +337,7 @@ class Group(EntryPoint, dict):
|
|
|
325
337
|
if condition:
|
|
326
338
|
if not condition(obj):
|
|
327
339
|
return
|
|
328
|
-
self.cmd(
|
|
340
|
+
self.cmd(method)
|
|
329
341
|
|
|
330
342
|
def __call__(self, *argv):
|
|
331
343
|
self.exit_code = 0
|
|
@@ -648,9 +660,14 @@ class Command(EntryPoint, dict):
|
|
|
648
660
|
except KeyboardInterrupt:
|
|
649
661
|
print('exiting cleanly...')
|
|
650
662
|
self.exit_code = 1
|
|
663
|
+
except Exception as exc:
|
|
664
|
+
self.handle_exception(exc)
|
|
651
665
|
finally:
|
|
652
666
|
self.post_result = self.post_call()
|
|
653
667
|
|
|
668
|
+
def handle_exception(self, exc):
|
|
669
|
+
raise exc
|
|
670
|
+
|
|
654
671
|
async def async_call(self, *argv):
|
|
655
672
|
""" Call with async stuff in single event loop """
|
|
656
673
|
error = self.parse(*argv)
|
|
@@ -665,8 +682,11 @@ class Command(EntryPoint, dict):
|
|
|
665
682
|
|
|
666
683
|
await self.factories_resolve()
|
|
667
684
|
|
|
668
|
-
|
|
669
|
-
|
|
685
|
+
try:
|
|
686
|
+
result = self.call(*self.bound.args, **self.bound.kwargs)
|
|
687
|
+
return await async_resolve(result, output=True)
|
|
688
|
+
except Exception as exc:
|
|
689
|
+
self.handle_exception(exc)
|
|
670
690
|
|
|
671
691
|
async def factories_resolve(self):
|
|
672
692
|
""" Resolve all factories values. """
|
|
@@ -825,7 +845,11 @@ class Argument:
|
|
|
825
845
|
self.type = param.annotation
|
|
826
846
|
|
|
827
847
|
self.negate = None
|
|
828
|
-
if
|
|
848
|
+
if (
|
|
849
|
+
self.iskw
|
|
850
|
+
and self.param.annotation == bool
|
|
851
|
+
and self.param.default is not False
|
|
852
|
+
):
|
|
829
853
|
self.negate = 'no-' + param.name
|
|
830
854
|
if cmd.posix:
|
|
831
855
|
self.negate = self.negate.replace('_', '-')
|
|
@@ -978,7 +1002,11 @@ class Argument:
|
|
|
978
1002
|
+ colors.reset
|
|
979
1003
|
)
|
|
980
1004
|
|
|
981
|
-
if
|
|
1005
|
+
if (
|
|
1006
|
+
self.type == bool
|
|
1007
|
+
and not self.negates
|
|
1008
|
+
and self.param.default is not False
|
|
1009
|
+
):
|
|
982
1010
|
self.cmd.print(
|
|
983
1011
|
'Accepted: '
|
|
984
1012
|
+ colors.blue3
|
|
@@ -1035,7 +1063,7 @@ class Argument:
|
|
|
1035
1063
|
if self.default != self.param.empty:
|
|
1036
1064
|
return self.default
|
|
1037
1065
|
msg = f'{self.param.name} has no CLI bound value nor default'
|
|
1038
|
-
raise
|
|
1066
|
+
raise Cli2ValueError(msg) from exc
|
|
1039
1067
|
|
|
1040
1068
|
@value.setter
|
|
1041
1069
|
def value(self, value):
|
|
@@ -8,6 +8,8 @@ The developer story we are after:
|
|
|
8
8
|
- when there's a minute to be nice to the user, add some help that will be
|
|
9
9
|
displayed to them: ``cli2.cfg.questions['YOUR_ENV_VAR'] = 'this is a help
|
|
10
10
|
text that will be display to the user when we prompt them for YOUR_ENV_VAR'``
|
|
11
|
+
- you can also set ``cli2.cfg.defaults['YOUR_ENV_VAR']`` if you prefer a
|
|
12
|
+
default value to an interactive prompt
|
|
11
13
|
|
|
12
14
|
This is the user story we are after:
|
|
13
15
|
|
|
@@ -29,6 +31,8 @@ import shlex
|
|
|
29
31
|
import textwrap
|
|
30
32
|
from pathlib import Path
|
|
31
33
|
|
|
34
|
+
from .log import log
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class Configuration(dict):
|
|
34
38
|
"""
|
|
@@ -43,6 +47,12 @@ class Configuration(dict):
|
|
|
43
47
|
configuration then question_string will be used as text to prompt the
|
|
44
48
|
user.
|
|
45
49
|
|
|
50
|
+
.. py:attribute:: defaults
|
|
51
|
+
|
|
52
|
+
A dict of ``ENV_VAR='default_value'``, if an env var is missing from
|
|
53
|
+
configuration then the default value will be returned instead of
|
|
54
|
+
displaying an interactive prompt.
|
|
55
|
+
|
|
46
56
|
.. py:attribute:: profile_path
|
|
47
57
|
|
|
48
58
|
Path to the shell profile to save/read variables, defaults to
|
|
@@ -67,8 +77,9 @@ class Configuration(dict):
|
|
|
67
77
|
api_url = cli2.cfg['USERNAME']
|
|
68
78
|
|
|
69
79
|
"""
|
|
70
|
-
def __init__(self, profile_path=None, **questions):
|
|
80
|
+
def __init__(self, profile_path=None, defaults=None, **questions):
|
|
71
81
|
self.questions = questions
|
|
82
|
+
self.defaults = defaults or dict()
|
|
72
83
|
self.profile_path = Path(
|
|
73
84
|
profile_path or os.getenv('HOME') + '/.profile'
|
|
74
85
|
)
|
|
@@ -155,6 +166,11 @@ class Configuration(dict):
|
|
|
155
166
|
if key in self.profile_variables:
|
|
156
167
|
return self.profile_variables[key]
|
|
157
168
|
|
|
169
|
+
if key in self.defaults:
|
|
170
|
+
value = self.defaults[key]
|
|
171
|
+
log.debug(f'Defaulting {key} to {value}')
|
|
172
|
+
return value
|
|
173
|
+
|
|
158
174
|
prompt = self.questions.get(key, key)
|
|
159
175
|
prompt = textwrap.dedent(prompt).strip()
|
|
160
176
|
value = self.input(prompt)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
""" Interactive user inputs """
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from .log import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def choice(question, choices=None, default=None):
|
|
11
|
+
"""
|
|
12
|
+
Ask user to make a choice.
|
|
13
|
+
|
|
14
|
+
.. code-block::
|
|
15
|
+
|
|
16
|
+
accepted = cli2.choice('Accept terms?') == 'y'
|
|
17
|
+
|
|
18
|
+
:param question: String question to ask
|
|
19
|
+
:param choices: List of acceptable choices, y/n by default
|
|
20
|
+
:param default: Default value for when the user does not add a value.
|
|
21
|
+
"""
|
|
22
|
+
choices = [c.lower() for c in choices or ['y', 'n']]
|
|
23
|
+
|
|
24
|
+
if default:
|
|
25
|
+
choices_display = [
|
|
26
|
+
c.upper() if c == default.lower() else c
|
|
27
|
+
for c in choices
|
|
28
|
+
]
|
|
29
|
+
else:
|
|
30
|
+
choices_display = choices
|
|
31
|
+
|
|
32
|
+
question = question + f' ({"/".join(choices_display)})'
|
|
33
|
+
|
|
34
|
+
tries = 30
|
|
35
|
+
while tries:
|
|
36
|
+
answer = input(question)
|
|
37
|
+
if not answer and default:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
if answer.lower() in choices:
|
|
41
|
+
return answer.lower()
|
|
42
|
+
|
|
43
|
+
tries -= 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def editor(content=None):
|
|
47
|
+
"""
|
|
48
|
+
Open $EDITOR with content, return the result.
|
|
49
|
+
|
|
50
|
+
Like git rebase -i does!
|
|
51
|
+
|
|
52
|
+
:param content: Initial content if any
|
|
53
|
+
:return: The edited content after $EDITOR exit
|
|
54
|
+
"""
|
|
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
|
+
editor = os.getenv('EDITOR', 'vim')
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
command = f"{editor} {shlex.quote(filepath)}"
|
|
65
|
+
subprocess.run(shlex.split(command), check=True)
|
|
66
|
+
|
|
67
|
+
with open(filepath, 'r') as f:
|
|
68
|
+
content = f.read()
|
|
69
|
+
return content
|
|
70
|
+
except subprocess.CalledProcessError as e:
|
|
71
|
+
log.error(f"Error running Vim: {e}")
|
|
72
|
+
return None
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
log.warn(f"Temporary file gone?? {filepath}")
|
|
75
|
+
return None
|
|
76
|
+
finally:
|
|
77
|
+
try:
|
|
78
|
+
os.remove(filepath)
|
|
79
|
+
except OSError as e:
|
|
80
|
+
log.warn(f"Error deleting temporary file {filepath}: {e}")
|
|
@@ -94,7 +94,6 @@ def configure(log_file=None):
|
|
|
94
94
|
if os.getenv('DEBUG'):
|
|
95
95
|
LOG_LEVEL = 'DEBUG'
|
|
96
96
|
|
|
97
|
-
timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S')
|
|
98
97
|
pre_chain = [
|
|
99
98
|
# add log level and timestamp to event_dict
|
|
100
99
|
structlog.stdlib.add_log_level,
|
|
@@ -102,9 +101,12 @@ def configure(log_file=None):
|
|
|
102
101
|
# that values in the extra parameters of log methods pass through to
|
|
103
102
|
# log output
|
|
104
103
|
structlog.stdlib.ExtraAdder(),
|
|
105
|
-
timestamper,
|
|
106
104
|
]
|
|
107
105
|
|
|
106
|
+
if 'NO_TIMESTAMPER' not in os.environ:
|
|
107
|
+
timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S')
|
|
108
|
+
pre_chain.append(timestamper)
|
|
109
|
+
|
|
108
110
|
cmd = '_'.join([
|
|
109
111
|
re.sub('[^0-9a-zA-Z]+', '_', arg.split('/')[-1])
|
|
110
112
|
for arg in sys.argv
|
|
@@ -213,18 +215,23 @@ def configure(log_file=None):
|
|
|
213
215
|
|
|
214
216
|
logging.config.dictConfig(LOGGING)
|
|
215
217
|
|
|
218
|
+
processors = [
|
|
219
|
+
structlog.stdlib.add_log_level,
|
|
220
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
221
|
+
]
|
|
222
|
+
if 'NO_TIMESTAMPER' not in os.environ:
|
|
223
|
+
processors.append(timestamper)
|
|
224
|
+
processors += [
|
|
225
|
+
structlog.processors.StackInfoRenderer(),
|
|
226
|
+
structlog.processors.format_exc_info,
|
|
227
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
228
|
+
]
|
|
229
|
+
|
|
216
230
|
structlog.configure(
|
|
217
231
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
218
232
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
219
233
|
cache_logger_on_first_use=True,
|
|
220
|
-
processors=
|
|
221
|
-
structlog.stdlib.add_log_level,
|
|
222
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
223
|
-
timestamper,
|
|
224
|
-
structlog.processors.StackInfoRenderer(),
|
|
225
|
-
structlog.processors.format_exc_info,
|
|
226
|
-
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
227
|
-
],
|
|
234
|
+
processors=processors,
|
|
228
235
|
)
|
|
229
236
|
|
|
230
237
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Like levenshtein with difflib.
|
|
3
|
+
|
|
4
|
+
.. code-block:: python
|
|
5
|
+
|
|
6
|
+
source_word = "apple"
|
|
7
|
+
word_list = ["apply", "aple", "banana", "orange", "applet"]
|
|
8
|
+
|
|
9
|
+
closest = cli2.closest(source_word, word_list)
|
|
10
|
+
print(f"The closest word to '{source_word}' is: {closest}")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import difflib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def closest(source_token, token_list):
|
|
17
|
+
"""
|
|
18
|
+
Finds the token in token_list with the shortest distance to source_token.
|
|
19
|
+
|
|
20
|
+
:param source_token: The source token (string).
|
|
21
|
+
:param token_list: A list of tokens (strings).
|
|
22
|
+
:return: The token with the shortest distance, or None if token_list is
|
|
23
|
+
empty.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if not token_list:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
closest_token = None
|
|
30
|
+
shortest_distance = float("inf") # Initialize with infinity
|
|
31
|
+
|
|
32
|
+
for token in token_list:
|
|
33
|
+
matcher = difflib.SequenceMatcher(None, source_token, token)
|
|
34
|
+
distance = (
|
|
35
|
+
1 - matcher.ratio()
|
|
36
|
+
) # Calculate a distance metric (1 - similarity ratio)
|
|
37
|
+
|
|
38
|
+
if distance < shortest_distance:
|
|
39
|
+
shortest_distance = distance
|
|
40
|
+
closest_token = token
|
|
41
|
+
|
|
42
|
+
return closest_token
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def closest_path(path, paths):
|
|
46
|
+
"""
|
|
47
|
+
Find the closest path from paths.
|
|
48
|
+
|
|
49
|
+
LLM may output broken paths, this fixes them.
|
|
50
|
+
|
|
51
|
+
:param path: Path to find closest
|
|
52
|
+
:param paths: List of paths to search in.
|
|
53
|
+
"""
|
|
54
|
+
parts = path.split('/')
|
|
55
|
+
for number, part in enumerate(parts):
|
|
56
|
+
paths_parts = {
|
|
57
|
+
str(path).split('/')[number]
|
|
58
|
+
for path in paths
|
|
59
|
+
}
|
|
60
|
+
if part not in paths_parts:
|
|
61
|
+
path_part = closest(part, paths_parts)
|
|
62
|
+
if not path_part:
|
|
63
|
+
return None # not found at all
|
|
64
|
+
else:
|
|
65
|
+
path_part = part
|
|
66
|
+
|
|
67
|
+
parts[number] = path_part
|
|
68
|
+
paths = {
|
|
69
|
+
path
|
|
70
|
+
for path in paths
|
|
71
|
+
if str(path).startswith('/'.join(parts[:number + 1]))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return '/'.join(parts)
|
|
@@ -184,7 +184,7 @@ class Table(list):
|
|
|
184
184
|
if len(wrapped) > 1:
|
|
185
185
|
leftovers[colnum] = ' '.join(wrapped[1:])
|
|
186
186
|
if color:
|
|
187
|
-
line[-1] = color + line[-1] + colors.reset
|
|
187
|
+
line[-1] = str(color) + line[-1] + colors.reset
|
|
188
188
|
for leftover in leftovers:
|
|
189
189
|
if len(leftover):
|
|
190
190
|
rows.insert(0, leftovers)
|
|
@@ -21,8 +21,8 @@ def autotest(path, cmd, ignore=None, env=None):
|
|
|
21
21
|
"""
|
|
22
22
|
environ = copy.copy(os.environ)
|
|
23
23
|
if env:
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
environ.update(env)
|
|
25
|
+
environ['CLI2_THEME'] = 'standard'
|
|
26
26
|
environ['FORCE_TERMSIZE'] = '1'
|
|
27
27
|
environ['PATH'] = ':'.join([
|
|
28
28
|
environ.get('HOME', '') + '/.local/bin',
|
|
@@ -75,7 +75,7 @@ def autotest(path, cmd, ignore=None, env=None):
|
|
|
75
75
|
'''.strip(),
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
diff_cmd = 'diff -U 1
|
|
78
|
+
diff_cmd = 'diff -U 1 "%s" - | sed "1,2 d"' % path
|
|
79
79
|
proc = subprocess.Popen(
|
|
80
80
|
diff_cmd,
|
|
81
81
|
stdout=subprocess.PIPE,
|
|
@@ -88,8 +88,8 @@ def autotest(path, cmd, ignore=None, env=None):
|
|
|
88
88
|
if diff_out:
|
|
89
89
|
raise type(f'''
|
|
90
90
|
DiffFound
|
|
91
|
-
- {
|
|
92
|
-
+ {
|
|
91
|
+
- {path}
|
|
92
|
+
+ {cmd}
|
|
93
93
|
'''.strip(), (Exception,), {})('\n' + diff_out.decode('utf8'))
|
|
94
94
|
|
|
95
95
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Color themes.
|
|
3
|
+
|
|
4
|
+
The theme is available in the `cli2.t` namespace.
|
|
5
|
+
|
|
6
|
+
.. envar:: CLI2_THEME
|
|
7
|
+
|
|
8
|
+
The default is "standard" but "monokai" and "flashy" are also available.
|
|
9
|
+
Standard theme uses basic colors, and lets you override them with
|
|
10
|
+
environment variables: CLI2_RED, CLI2_GREEN, and so on.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
import cli2
|
|
17
|
+
|
|
18
|
+
print(f'{cli2.theme.green}OK{cli2.theme.reset}')
|
|
19
|
+
|
|
20
|
+
# with a mode, such as bold, dim, italic, underline and strike:
|
|
21
|
+
print(f'{cli2.theme.green.bold}OK{cli2.theme.reset}')
|
|
22
|
+
|
|
23
|
+
# all is also callable appending the reset automatically
|
|
24
|
+
print(cli2.theme.green('OK'))
|
|
25
|
+
print(cli2.theme.green.bold('OK'))
|
|
26
|
+
|
|
27
|
+
We also have shortcuts, ``cli2.theme`` is ``cli2.t``, each color can be
|
|
28
|
+
referred to by first letter in lowercase, except for black and gray which are
|
|
29
|
+
refered to by their first letter in uppercase. Modes can be referred to by
|
|
30
|
+
first letter in lowercase too, and reset is rs:
|
|
31
|
+
|
|
32
|
+
.. code-block:: python
|
|
33
|
+
|
|
34
|
+
# shortcuts
|
|
35
|
+
print(f'{cli2.t.g.b}OK{cli2.t.rs}')
|
|
36
|
+
|
|
37
|
+
# shorter with callable
|
|
38
|
+
print(cli2.t.g.b('OK'))
|
|
39
|
+
|
|
40
|
+
Run ``cli2-theme`` for the list of colors by theme.
|
|
41
|
+
"""
|
|
42
|
+
import re
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
from .cli import Command
|
|
46
|
+
|
|
47
|
+
themes = dict(
|
|
48
|
+
standard=dict(
|
|
49
|
+
black=int(os.getenv('CLI2_BLACK', 0)),
|
|
50
|
+
red=int(os.getenv('CLI2_RED', 1)),
|
|
51
|
+
green=int(os.getenv('CLI2_GREEN', 2)),
|
|
52
|
+
yellow=int(os.getenv('CLI2_YELLOW', 3)),
|
|
53
|
+
orange=int(os.getenv('CLI2_ORANGE', 208)),
|
|
54
|
+
blue=int(os.getenv('CLI2_BLUE', 4)),
|
|
55
|
+
mauve=int(os.getenv('CLI2_MAUVE', 5)),
|
|
56
|
+
pink=int(os.getenv('CLI2_PINK', 164)),
|
|
57
|
+
cyan=int(os.getenv('CLI2_CYAN', 6)),
|
|
58
|
+
gray=int(os.getenv('CLI2_GRAY', 7)),
|
|
59
|
+
),
|
|
60
|
+
flashy=dict(
|
|
61
|
+
black=0,
|
|
62
|
+
red=196,
|
|
63
|
+
green=46,
|
|
64
|
+
yellow=227,
|
|
65
|
+
blue=27,
|
|
66
|
+
mauve=129,
|
|
67
|
+
pink=201,
|
|
68
|
+
orange=202,
|
|
69
|
+
cyan=51,
|
|
70
|
+
gray=253,
|
|
71
|
+
),
|
|
72
|
+
monokai=dict(
|
|
73
|
+
black=0,
|
|
74
|
+
red=124,
|
|
75
|
+
green=150,
|
|
76
|
+
yellow=179,
|
|
77
|
+
blue=67,
|
|
78
|
+
mauve=140,
|
|
79
|
+
pink=132,
|
|
80
|
+
orange=202,
|
|
81
|
+
cyan=80,
|
|
82
|
+
gray=246,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Renderer:
|
|
88
|
+
def __call__(self, *content):
|
|
89
|
+
return f'{self}{" ".join([str(c) for c in content])}{t.rs}'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Mode(Renderer):
|
|
93
|
+
def __init__(self, name, code):
|
|
94
|
+
self.name = name
|
|
95
|
+
self.code = code
|
|
96
|
+
self.alias = name[0]
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
return f'\u001b[{self.code}m'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ColorMode(Renderer):
|
|
103
|
+
def __init__(self, color, mode):
|
|
104
|
+
self.color = color
|
|
105
|
+
self.mode = mode
|
|
106
|
+
|
|
107
|
+
def __str__(self):
|
|
108
|
+
return f'\u001b[{self.mode.code};38;5;{self.color.code}m'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
modes = {
|
|
112
|
+
name: Mode(name, code)
|
|
113
|
+
for name, code in dict(
|
|
114
|
+
bold=1,
|
|
115
|
+
dim=2,
|
|
116
|
+
italic=3,
|
|
117
|
+
underline=4,
|
|
118
|
+
strike=9,
|
|
119
|
+
).items()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Color(Renderer):
|
|
124
|
+
def __init__(self, code, name=None, alias=None):
|
|
125
|
+
self.name = name
|
|
126
|
+
self.alias = alias
|
|
127
|
+
self.code = code
|
|
128
|
+
|
|
129
|
+
for mode, code in modes.items():
|
|
130
|
+
color_mode = ColorMode(self, code)
|
|
131
|
+
setattr(self, mode, color_mode)
|
|
132
|
+
setattr(self, mode[0], color_mode)
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return f'\u001b[38;5;{self.code}m'
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Theme:
|
|
139
|
+
def __init__(self, colors):
|
|
140
|
+
self.colors = colors
|
|
141
|
+
|
|
142
|
+
for name, mode in modes.items():
|
|
143
|
+
setattr(self, mode.name, mode)
|
|
144
|
+
|
|
145
|
+
for name, value in colors.items():
|
|
146
|
+
if name in ('black', 'gray'):
|
|
147
|
+
alias = name[0].upper()
|
|
148
|
+
else:
|
|
149
|
+
alias = name[0]
|
|
150
|
+
|
|
151
|
+
color = Color(value, name, alias)
|
|
152
|
+
setattr(self, name, color)
|
|
153
|
+
setattr(self, alias, color)
|
|
154
|
+
|
|
155
|
+
for name in ('reset', 'rs'):
|
|
156
|
+
setattr(self, name, '\u001b[0m')
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def len(string):
|
|
160
|
+
"""
|
|
161
|
+
Counts the number of alphabetic characters in a string,
|
|
162
|
+
ignoring ANSI escape codes.
|
|
163
|
+
|
|
164
|
+
:param string: Input string
|
|
165
|
+
:return: Integer count of actual printable chars
|
|
166
|
+
"""
|
|
167
|
+
# Regular expression to match ANSI escape codes
|
|
168
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
169
|
+
|
|
170
|
+
# Remove ANSI escape codes from the string
|
|
171
|
+
cleaned_text = ansi_escape.sub('', string)
|
|
172
|
+
|
|
173
|
+
# Count the alphabetic characters in the cleaned string
|
|
174
|
+
letter_count = 0
|
|
175
|
+
for char in cleaned_text:
|
|
176
|
+
if 'a' <= char <= 'z' or 'A' <= char <= 'Z':
|
|
177
|
+
letter_count += 1
|
|
178
|
+
|
|
179
|
+
return letter_count
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
t = theme = Theme(themes[os.getenv('CLI2_THEME', 'standard')])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def demo():
|
|
186
|
+
"""
|
|
187
|
+
Print all colors and modes from the theme.
|
|
188
|
+
"""
|
|
189
|
+
for name, theme in themes.items():
|
|
190
|
+
theme = Theme(themes[name])
|
|
191
|
+
print(f'\n\nTheme: {theme.bold(name)}')
|
|
192
|
+
_demo(theme)
|
|
193
|
+
|
|
194
|
+
print()
|
|
195
|
+
print(theme.bold('MODES:'))
|
|
196
|
+
for name in modes:
|
|
197
|
+
mode = getattr(theme, name)
|
|
198
|
+
print(f'{mode}t.{name}{t.rs}')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _demo(theme):
|
|
202
|
+
def color_data(alias, color):
|
|
203
|
+
data = [
|
|
204
|
+
(color, alias),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for mode in modes:
|
|
208
|
+
data.append(
|
|
209
|
+
(getattr(color, mode[0]), f'{alias}.{mode[0]}'),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
from .table import Table
|
|
215
|
+
table = Table()
|
|
216
|
+
for name in t.colors:
|
|
217
|
+
color = getattr(theme, name)
|
|
218
|
+
|
|
219
|
+
table.append(color_data(color.alias, color))
|
|
220
|
+
table.print()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
cli = Command(demo)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.1rc1
|
|
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,5 +63,6 @@ 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
67
|
|
|
67
68
|
`Documentation available on RTFD <https://cli2.rtfd.io>`_.
|