cli2 2.6.3__tar.gz → 3.1.0.dev1__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-2.6.3/cli2.egg-info → cli2-3.1.0.dev1}/PKG-INFO +5 -2
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/__init__.py +1 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/argument.py +21 -1
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/colors.py +16 -0
- cli2-3.1.0.dev1/cli2/command.py +410 -0
- cli2-3.1.0.dev1/cli2/display.py +98 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/entry_point.py +4 -9
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/group.py +9 -2
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_command.py +202 -26
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_decorators.py +2 -2
- cli2-3.1.0.dev1/cli2/test_display.py +53 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_group.py +34 -1
- {cli2-2.6.3 → cli2-3.1.0.dev1/cli2.egg-info}/PKG-INFO +5 -2
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2.egg-info/SOURCES.txt +2 -1
- cli2-3.1.0.dev1/cli2.egg-info/requires.txt +9 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/setup.py +7 -2
- cli2-2.6.3/cli2/command.py +0 -184
- cli2-2.6.3/cli2/test_entrypoint.py +0 -17
- cli2-2.6.3/cli2.egg-info/requires.txt +0 -6
- {cli2-2.6.3 → cli2-3.1.0.dev1}/MANIFEST.in +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/README.rst +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/classifiers.txt +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/cli.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/decorators.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/node.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/table.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_cli.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_inject.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_node.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2/test_table.py +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/cli2.egg-info/top_level.txt +0 -0
- {cli2-2.6.3 → cli2-3.1.0.dev1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.1.0.dev1
|
|
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
|
|
@@ -9,11 +9,14 @@ License: MIT
|
|
|
9
9
|
Keywords: cli
|
|
10
10
|
Requires-Python: >=3.6
|
|
11
11
|
Description-Content-Type: text/x-rst
|
|
12
|
-
Requires-Dist: docstring_parser
|
|
12
|
+
Requires-Dist: docstring_parser
|
|
13
|
+
Requires-Dist: pyyaml
|
|
14
|
+
Requires-Dist: pygments
|
|
13
15
|
Provides-Extra: test
|
|
14
16
|
Requires-Dist: freezegun; extra == "test"
|
|
15
17
|
Requires-Dist: pytest; extra == "test"
|
|
16
18
|
Requires-Dist: pytest-cov; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
17
20
|
|
|
18
21
|
.. image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
|
|
19
22
|
:target: https://yourlabs.io/oss/cli2/pipelines
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import re
|
|
2
3
|
import json
|
|
3
4
|
|
|
@@ -10,12 +11,14 @@ class Argument:
|
|
|
10
11
|
"""
|
|
11
12
|
# TODO: why not split this into a bunch of simpler sub-classes now that
|
|
12
13
|
# it's pretty featureful ?
|
|
13
|
-
def __init__(self, cmd, param, doc=None, color=None,
|
|
14
|
+
def __init__(self, cmd, param, doc=None, color=None, factory=None,
|
|
15
|
+
**kwargs):
|
|
14
16
|
self.cmd = cmd
|
|
15
17
|
self.param = param
|
|
16
18
|
self.color = color
|
|
17
19
|
# Let default be set to None :)
|
|
18
20
|
self.default = kwargs.pop('default', param.default)
|
|
21
|
+
self.factory = factory
|
|
19
22
|
|
|
20
23
|
self.doc = doc or ''
|
|
21
24
|
if not doc:
|
|
@@ -362,3 +365,20 @@ class Argument:
|
|
|
362
365
|
if value is not None:
|
|
363
366
|
self.value = self.cast(value)
|
|
364
367
|
return True
|
|
368
|
+
|
|
369
|
+
def factory_value(self):
|
|
370
|
+
"""
|
|
371
|
+
Run the factory function and return the value.
|
|
372
|
+
|
|
373
|
+
If the factory function takes a `cmd` argument, it will pass the
|
|
374
|
+
command object.
|
|
375
|
+
|
|
376
|
+
If the factory function takes an `arg` argument, it will pass self.
|
|
377
|
+
"""
|
|
378
|
+
kwargs = dict()
|
|
379
|
+
sig = inspect.signature(self.factory)
|
|
380
|
+
if 'cmd' in sig.parameters:
|
|
381
|
+
kwargs['cmd'] = self.cmd
|
|
382
|
+
if 'arg' in sig.parameters:
|
|
383
|
+
kwargs['arg'] = self
|
|
384
|
+
return self.factory(**kwargs)
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Define a bunch of arbitrary color ANSI color codes.
|
|
3
|
+
|
|
4
|
+
This module is available in the `cli2.c` namespace.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
|
|
8
|
+
.. code-block:: python
|
|
9
|
+
|
|
10
|
+
import cli2
|
|
11
|
+
print(f'{cli2.c.green2bold}OK{cli2.c.reset}')
|
|
12
|
+
|
|
13
|
+
See the following for details.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
colors = dict(
|
|
2
18
|
cyan='\u001b[38;5;51m',
|
|
3
19
|
cyan1='\u001b[38;5;87m',
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from docstring_parser import parse
|
|
6
|
+
|
|
7
|
+
from . import display
|
|
8
|
+
from .argument import Argument
|
|
9
|
+
from .colors import colors
|
|
10
|
+
from .entry_point import EntryPoint
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Command(EntryPoint, dict):
|
|
14
|
+
"""Represents a command bound to a target callable."""
|
|
15
|
+
|
|
16
|
+
def __new__(cls, target, *args, **kwargs):
|
|
17
|
+
overrides = getattr(target, 'cli2', {})
|
|
18
|
+
cls = overrides.get('cls', cls)
|
|
19
|
+
return super().__new__(cls, *args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def __init__(self, target, name=None, color=None, doc=None, posix=False,
|
|
22
|
+
help_hack=True, outfile=None, log=True):
|
|
23
|
+
self.target = target
|
|
24
|
+
self.posix = posix
|
|
25
|
+
self.parent = None
|
|
26
|
+
self.help_hack = help_hack
|
|
27
|
+
|
|
28
|
+
overrides = getattr(target, 'cli2', {})
|
|
29
|
+
for key, value in overrides.items():
|
|
30
|
+
setattr(self, key, value)
|
|
31
|
+
|
|
32
|
+
if name:
|
|
33
|
+
self.name = name
|
|
34
|
+
elif 'name' not in overrides:
|
|
35
|
+
self.name = getattr(target, '__name__', type(target).__name__)
|
|
36
|
+
|
|
37
|
+
self.parsed = parse(inspect.getdoc(target))
|
|
38
|
+
if doc:
|
|
39
|
+
self.doc = doc
|
|
40
|
+
elif 'doc' not in overrides:
|
|
41
|
+
self.doc = ''
|
|
42
|
+
if self.parsed.short_description:
|
|
43
|
+
self.doc += self.parsed.short_description.replace('\n', ' ')
|
|
44
|
+
if self.parsed.long_description:
|
|
45
|
+
if self.doc:
|
|
46
|
+
self.doc += '\n'
|
|
47
|
+
self.doc += self.parsed.long_description
|
|
48
|
+
|
|
49
|
+
if color:
|
|
50
|
+
self.color = color
|
|
51
|
+
elif 'color' not in overrides:
|
|
52
|
+
self.color = 'orange'
|
|
53
|
+
|
|
54
|
+
self.positions = dict()
|
|
55
|
+
self.sig = inspect.signature(target)
|
|
56
|
+
self.setargs()
|
|
57
|
+
EntryPoint.__init__(self, outfile=outfile, log=log)
|
|
58
|
+
|
|
59
|
+
def setargs(self):
|
|
60
|
+
"""Reset arguments."""
|
|
61
|
+
for name, param in self.sig.parameters.items():
|
|
62
|
+
overrides = getattr(self.target, 'cli2_' + name, {})
|
|
63
|
+
cls = overrides.get('cls', Argument)
|
|
64
|
+
self[name] = cls(self, param)
|
|
65
|
+
for key, value in overrides.items():
|
|
66
|
+
setattr(self[name], key, value)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def cmd(cls, *args, **kwargs):
|
|
70
|
+
def override(target):
|
|
71
|
+
overrides = getattr(target, 'cli2', {})
|
|
72
|
+
overrides.update(kwargs)
|
|
73
|
+
overrides['cls'] = cls
|
|
74
|
+
target.cli2 = overrides
|
|
75
|
+
|
|
76
|
+
if len(args) == 1 and not kwargs:
|
|
77
|
+
# simple @YourCommand.cmd syntax
|
|
78
|
+
target = args[0]
|
|
79
|
+
override(target)
|
|
80
|
+
return target
|
|
81
|
+
elif not args:
|
|
82
|
+
def wrap(cb):
|
|
83
|
+
override(cb)
|
|
84
|
+
return cb
|
|
85
|
+
return wrap
|
|
86
|
+
else:
|
|
87
|
+
raise Exception('Only kwargs are supported by Group.cmd')
|
|
88
|
+
|
|
89
|
+
def help(self, error=None, short=False, missing=None):
|
|
90
|
+
"""Show help for a command."""
|
|
91
|
+
if short:
|
|
92
|
+
if self.doc:
|
|
93
|
+
return self.doc.replace('\n', ' ').split('.')[0]
|
|
94
|
+
return ''
|
|
95
|
+
|
|
96
|
+
if missing:
|
|
97
|
+
error = (
|
|
98
|
+
f'missing {len(missing)} required argument'
|
|
99
|
+
f'{"s" if len(missing) > 1 else ""}'
|
|
100
|
+
f': {", ".join(missing)}'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if error:
|
|
104
|
+
self.print('RED', 'ERROR: ' + colors.reset + error, end='\n\n')
|
|
105
|
+
|
|
106
|
+
self.print('ORANGE', 'SYNOPSYS')
|
|
107
|
+
chain = []
|
|
108
|
+
current = self
|
|
109
|
+
while current is not None:
|
|
110
|
+
chain.insert(0, current.name)
|
|
111
|
+
current = current.parent
|
|
112
|
+
for arg in self.values():
|
|
113
|
+
chain.append(str(arg))
|
|
114
|
+
self.print(' '.join(map(str, chain)), end='\n\n')
|
|
115
|
+
|
|
116
|
+
self.print('ORANGE', 'DESCRIPTION')
|
|
117
|
+
self.print(self.doc)
|
|
118
|
+
|
|
119
|
+
shown_posargs = False
|
|
120
|
+
shown_kwargs = False
|
|
121
|
+
for arg in self.values():
|
|
122
|
+
self.print()
|
|
123
|
+
|
|
124
|
+
if not arg.iskw and not shown_posargs:
|
|
125
|
+
self.print('ORANGE', 'POSITIONAL ARGUMENTS')
|
|
126
|
+
shown_posargs = True
|
|
127
|
+
|
|
128
|
+
varkw = arg.param.kind == arg.param.VAR_KEYWORD
|
|
129
|
+
if (arg.iskw or varkw) and not shown_kwargs:
|
|
130
|
+
self.print('ORANGE', 'NAMED ARGUMENTS')
|
|
131
|
+
shown_kwargs = True
|
|
132
|
+
arg.help()
|
|
133
|
+
|
|
134
|
+
def parse(self, *argv):
|
|
135
|
+
"""Parse arguments into BoundArguments."""
|
|
136
|
+
self.setargs()
|
|
137
|
+
self.bound = self.sig.bind_partial()
|
|
138
|
+
extra = []
|
|
139
|
+
for current in argv:
|
|
140
|
+
taken = False
|
|
141
|
+
for arg in self.values():
|
|
142
|
+
taken = arg.take(current)
|
|
143
|
+
if taken:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
if not taken:
|
|
147
|
+
extra.append(current)
|
|
148
|
+
|
|
149
|
+
if extra:
|
|
150
|
+
return 'No parameters for these arguments: ' + ', '.join(extra)
|
|
151
|
+
|
|
152
|
+
for name, arg in self.items(factories=None):
|
|
153
|
+
if arg.factory:
|
|
154
|
+
if self.async_function(arg.factory):
|
|
155
|
+
arg.value = 'to_be_computed'
|
|
156
|
+
else:
|
|
157
|
+
arg.value = arg.factory_value()
|
|
158
|
+
continue
|
|
159
|
+
if not arg.default:
|
|
160
|
+
continue
|
|
161
|
+
if name in self.bound.arguments:
|
|
162
|
+
continue
|
|
163
|
+
arg.value = arg.default
|
|
164
|
+
|
|
165
|
+
def async_function(self, function):
|
|
166
|
+
""" Return True if function is async """
|
|
167
|
+
return (
|
|
168
|
+
inspect.iscoroutinefunction(function)
|
|
169
|
+
or inspect.isasyncgenfunction(function)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def async_mode(self):
|
|
173
|
+
""" Return True if any callable we'll deal with is async """
|
|
174
|
+
for arg in self.values():
|
|
175
|
+
if self.async_function(arg.factory):
|
|
176
|
+
return True
|
|
177
|
+
if (
|
|
178
|
+
self.async_function(self.target)
|
|
179
|
+
or self.async_function(self.post_call)
|
|
180
|
+
):
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
for name, arg in self.items(factories=True):
|
|
184
|
+
if arg.factory and self.async_function(arg.factory):
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def async_iter(self, obj):
|
|
189
|
+
return inspect.isasyncgen(obj) or hasattr(obj, '__aiter__')
|
|
190
|
+
|
|
191
|
+
async def async_resolve(self, result, output=False):
|
|
192
|
+
""" Recursively resolve awaitables. """
|
|
193
|
+
while inspect.iscoroutine(result):
|
|
194
|
+
result = await result
|
|
195
|
+
if self.async_iter(result):
|
|
196
|
+
results = []
|
|
197
|
+
async for _ in result:
|
|
198
|
+
if output:
|
|
199
|
+
if (
|
|
200
|
+
not inspect.iscoroutine(_)
|
|
201
|
+
and not inspect.isasyncgen(_)
|
|
202
|
+
):
|
|
203
|
+
display.print(_)
|
|
204
|
+
else:
|
|
205
|
+
await self.async_resolve(_, output=output)
|
|
206
|
+
else:
|
|
207
|
+
results.append(await self.async_resolve(_))
|
|
208
|
+
return None if output else results
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
def call(self, *args, **kwargs):
|
|
212
|
+
"""Execute command target with bound arguments."""
|
|
213
|
+
return self.target(*args, **kwargs)
|
|
214
|
+
|
|
215
|
+
def missing(self):
|
|
216
|
+
return [
|
|
217
|
+
name
|
|
218
|
+
for name, arg in self.items()
|
|
219
|
+
if name not in self.bound.arguments
|
|
220
|
+
and name not in self.bound.kwargs
|
|
221
|
+
and arg.param.default == arg.param.empty
|
|
222
|
+
and arg.param.kind in (
|
|
223
|
+
arg.param.POSITIONAL_ONLY,
|
|
224
|
+
arg.param.POSITIONAL_OR_KEYWORD,
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
def __call__(self, *argv):
|
|
229
|
+
"""Execute command with args from sysargs."""
|
|
230
|
+
self.exit_code = 0
|
|
231
|
+
|
|
232
|
+
if self.help_hack and '--help' in argv:
|
|
233
|
+
self.exit_code = 1
|
|
234
|
+
return self.help()
|
|
235
|
+
|
|
236
|
+
if self.async_mode():
|
|
237
|
+
return asyncio.run(self.async_call(*argv))
|
|
238
|
+
|
|
239
|
+
error = self.parse(*argv)
|
|
240
|
+
if error:
|
|
241
|
+
self.exit_code = 1
|
|
242
|
+
return self.help(error=error)
|
|
243
|
+
|
|
244
|
+
missing = self.missing()
|
|
245
|
+
if missing:
|
|
246
|
+
self.exit_code = 1
|
|
247
|
+
return self.help(missing=missing)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
result = self.call(*self.bound.args, **self.bound.kwargs)
|
|
251
|
+
if (
|
|
252
|
+
inspect.isgenerator(result)
|
|
253
|
+
or isinstance(result, (list, tuple))
|
|
254
|
+
):
|
|
255
|
+
for _ in result:
|
|
256
|
+
display.print(_)
|
|
257
|
+
result = None
|
|
258
|
+
except KeyboardInterrupt:
|
|
259
|
+
print('exiting')
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
finally:
|
|
262
|
+
self.post_result = self.post_call()
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
async def async_call(self, *argv):
|
|
266
|
+
""" Call with async stuff in single event loop """
|
|
267
|
+
error = self.parse(*argv)
|
|
268
|
+
if error:
|
|
269
|
+
self.exit_code = 1
|
|
270
|
+
return self.help(error=error)
|
|
271
|
+
|
|
272
|
+
missing = self.missing()
|
|
273
|
+
if missing:
|
|
274
|
+
self.exit_code = 1
|
|
275
|
+
return self.help(missing=missing)
|
|
276
|
+
|
|
277
|
+
factories = self.values(factories=True)
|
|
278
|
+
if factories:
|
|
279
|
+
results = await asyncio.gather(*[
|
|
280
|
+
self.async_resolve(arg.factory_value())
|
|
281
|
+
for arg in factories
|
|
282
|
+
])
|
|
283
|
+
for _, arg in enumerate(factories):
|
|
284
|
+
arg.value = results[_]
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
result = self.call(*self.bound.args, **self.bound.kwargs)
|
|
288
|
+
result = await self.async_resolve(result, output=True)
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
print('exiting')
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
finally:
|
|
293
|
+
self.post_result = await self.async_resolve(self.post_call())
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
def ordered(self, factories=False):
|
|
297
|
+
"""
|
|
298
|
+
Order the parameters by priority.
|
|
299
|
+
|
|
300
|
+
:param factories: Show only arguments with factory.
|
|
301
|
+
"""
|
|
302
|
+
return {key: self[key] for key in self.keys(factories=factories)}
|
|
303
|
+
|
|
304
|
+
def values(self, factories=False):
|
|
305
|
+
"""
|
|
306
|
+
Return ordered values.
|
|
307
|
+
|
|
308
|
+
:param factories: Show only arguments with factory.
|
|
309
|
+
"""
|
|
310
|
+
return self.ordered(factories=factories).values()
|
|
311
|
+
|
|
312
|
+
def items(self, factories=False):
|
|
313
|
+
"""
|
|
314
|
+
Return ordered items.
|
|
315
|
+
|
|
316
|
+
:param factories: Show only arguments with factory.
|
|
317
|
+
"""
|
|
318
|
+
return self.ordered(factories=factories).items()
|
|
319
|
+
|
|
320
|
+
def keys(self, factories=False):
|
|
321
|
+
"""
|
|
322
|
+
Return ordered keys.
|
|
323
|
+
|
|
324
|
+
:param factories: Show only arguments with factory.
|
|
325
|
+
"""
|
|
326
|
+
order = (
|
|
327
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
328
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
329
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
330
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
331
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
332
|
+
)
|
|
333
|
+
keys = []
|
|
334
|
+
for kind in order:
|
|
335
|
+
for name, arg in super().items():
|
|
336
|
+
if factories is False and arg.factory:
|
|
337
|
+
continue
|
|
338
|
+
if factories is True and not arg.factory:
|
|
339
|
+
continue
|
|
340
|
+
if name in self.positions:
|
|
341
|
+
continue
|
|
342
|
+
if arg.param.kind == kind:
|
|
343
|
+
keys.append(name)
|
|
344
|
+
for key, position in self.positions.items():
|
|
345
|
+
if factories and not self[key].factory:
|
|
346
|
+
continue
|
|
347
|
+
keys.insert(position, key)
|
|
348
|
+
return keys
|
|
349
|
+
|
|
350
|
+
def __iter__(self):
|
|
351
|
+
return self.ordered().__iter__()
|
|
352
|
+
|
|
353
|
+
def arg(
|
|
354
|
+
self,
|
|
355
|
+
name,
|
|
356
|
+
*,
|
|
357
|
+
kind: str = None,
|
|
358
|
+
position: int = None,
|
|
359
|
+
doc=None,
|
|
360
|
+
color=None,
|
|
361
|
+
default=inspect.Parameter.empty,
|
|
362
|
+
annotation=inspect.Parameter.empty,
|
|
363
|
+
):
|
|
364
|
+
"""
|
|
365
|
+
Inject new :py:class:`~cli2.argument.Argument` into this command.
|
|
366
|
+
|
|
367
|
+
The new argument will appear in documentation, but won't be bound to
|
|
368
|
+
the callable: it will only be avalaible in `self`.
|
|
369
|
+
|
|
370
|
+
For example, you are deleting an "http_client" argument in
|
|
371
|
+
:py:meth:`setargs()` so that it doesn't appear to the CLI user, to whom
|
|
372
|
+
you want to expose a couple of arguments such as "base_url" and
|
|
373
|
+
"ssl_verify" that you are adding programatically with this method, so
|
|
374
|
+
that you can use `self['base_url'].value` and
|
|
375
|
+
`self['ssl_verify'].value` in to generate a "http_client" argument in
|
|
376
|
+
:py:meth:`call()`.
|
|
377
|
+
|
|
378
|
+
The tutorial has a more comprehensive example in the "CLI only
|
|
379
|
+
arguments" section.
|
|
380
|
+
|
|
381
|
+
:param name: Name of the argument to add
|
|
382
|
+
:param kind: Name of the inspect parameter kind
|
|
383
|
+
:param position: Position of the argument in the CLI
|
|
384
|
+
:param doc: Documentation for the argument
|
|
385
|
+
:param color: Color of the argument
|
|
386
|
+
:param default: Default value for the argument
|
|
387
|
+
:param annotation: Type of argument
|
|
388
|
+
"""
|
|
389
|
+
self[name] = Argument(
|
|
390
|
+
self,
|
|
391
|
+
inspect.Parameter(
|
|
392
|
+
name,
|
|
393
|
+
kind=getattr(
|
|
394
|
+
inspect.Parameter,
|
|
395
|
+
kind or "POSITIONAL_OR_KEYWORD",
|
|
396
|
+
),
|
|
397
|
+
default=default,
|
|
398
|
+
annotation=annotation,
|
|
399
|
+
),
|
|
400
|
+
doc=doc,
|
|
401
|
+
color=color,
|
|
402
|
+
)
|
|
403
|
+
if position is not None:
|
|
404
|
+
self.positions[name] = position
|
|
405
|
+
|
|
406
|
+
def post_call(self):
|
|
407
|
+
"""
|
|
408
|
+
Implement your cleaner here
|
|
409
|
+
"""
|
|
410
|
+
pass
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic pretty display utils.
|
|
3
|
+
|
|
4
|
+
This module defines a print function that's supposed to be able to pretty-print
|
|
5
|
+
anything, as well as a pretty diff printer.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
NO_COLOR = bool(os.getenv('NO_COLOR', ''))
|
|
11
|
+
_print = print
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def highlight(string, lexer):
|
|
15
|
+
if NO_COLOR:
|
|
16
|
+
return string
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import pygments
|
|
20
|
+
import pygments.lexers
|
|
21
|
+
import pygments.formatters
|
|
22
|
+
except ImportError:
|
|
23
|
+
return string
|
|
24
|
+
|
|
25
|
+
formatter = pygments.formatters.TerminalFormatter()
|
|
26
|
+
lexer = getattr(pygments.lexers, lexer + 'Lexer')()
|
|
27
|
+
return pygments.highlight(string, lexer, formatter)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def yaml_dump(data):
|
|
31
|
+
import yaml
|
|
32
|
+
if isinstance(data, dict):
|
|
33
|
+
# ensure that objects inheriting from dict render nicely
|
|
34
|
+
data = dict(data)
|
|
35
|
+
return yaml.dump(data, indent=4, width=float('inf'))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def yaml_highlight(yaml_string):
|
|
39
|
+
return highlight(yaml_string, 'Yaml')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print(*args, **kwargs):
|
|
43
|
+
"""
|
|
44
|
+
Try to print the args, pass the kwargs to actual print method.
|
|
45
|
+
|
|
46
|
+
If any arg is parseable as JSON then it'l be parsed.
|
|
47
|
+
|
|
48
|
+
Then, it'll be dumped as colored YAML.
|
|
49
|
+
|
|
50
|
+
Set the env var `NO_COLORS` to anything to
|
|
51
|
+
prevent `cli2.print` from printing colors.
|
|
52
|
+
|
|
53
|
+
.. code-block:: python
|
|
54
|
+
|
|
55
|
+
import cli2
|
|
56
|
+
|
|
57
|
+
# pretty print some_object
|
|
58
|
+
cli2.print(some_object)
|
|
59
|
+
|
|
60
|
+
This outputs colors by default, set the env var `NO_COLORS` to anything to
|
|
61
|
+
prevent printing colors.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
import jsonlight as json
|
|
65
|
+
except ImportError:
|
|
66
|
+
import json
|
|
67
|
+
|
|
68
|
+
for arg in args:
|
|
69
|
+
try: # deal with response objects
|
|
70
|
+
arg = arg.json()
|
|
71
|
+
except (TypeError, AttributeError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
try: # is this json?
|
|
75
|
+
arg = json.loads(arg)
|
|
76
|
+
except: # noqa
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
string = arg if isinstance(arg, str) else yaml_dump(arg)
|
|
80
|
+
_print(yaml_highlight(string), **kwargs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def diff_highlight(diff):
|
|
84
|
+
output = '\n'.join([line.rstrip() for line in diff if line.strip()])
|
|
85
|
+
return highlight(output, 'Diff')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def diff(diff, **kwargs):
|
|
89
|
+
"""
|
|
90
|
+
Pretty-print a diff generated by Python's standard difflib.unified_diff
|
|
91
|
+
method.
|
|
92
|
+
|
|
93
|
+
.. code-block:: python
|
|
94
|
+
|
|
95
|
+
# pretty print a diff
|
|
96
|
+
cli2.diff(difflib.unified_diff(old, new))
|
|
97
|
+
"""
|
|
98
|
+
_print(diff_highlight(diff), **kwargs)
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
from .colors import colors
|
|
6
|
-
from .
|
|
6
|
+
from . import display
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class EntryPoint:
|
|
@@ -28,14 +28,9 @@ class EntryPoint:
|
|
|
28
28
|
|
|
29
29
|
result = self(*args[1:])
|
|
30
30
|
if result is not None:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
except: # noqa
|
|
35
|
-
print(result)
|
|
36
|
-
else:
|
|
37
|
-
table.print()
|
|
38
|
-
else:
|
|
31
|
+
try:
|
|
32
|
+
display.print(result)
|
|
33
|
+
except: # noqa
|
|
39
34
|
print(result)
|
|
40
35
|
sys.exit(self.exit_code)
|
|
41
36
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import textwrap
|
|
2
3
|
|
|
3
4
|
from .colors import colors
|
|
4
5
|
from .command import Command
|
|
@@ -13,7 +14,10 @@ class Group(EntryPoint, dict):
|
|
|
13
14
|
def __init__(self, name=None, doc=None, color=None, posix=False,
|
|
14
15
|
outfile=None, cmdclass=None, log=True):
|
|
15
16
|
self.name = name
|
|
16
|
-
|
|
17
|
+
if doc:
|
|
18
|
+
self.doc = textwrap.dedent(doc).strip()
|
|
19
|
+
else:
|
|
20
|
+
self.doc = inspect.getdoc(self)
|
|
17
21
|
self.color = color or colors.green
|
|
18
22
|
self.posix = posix
|
|
19
23
|
self.parent = None
|
|
@@ -31,6 +35,8 @@ class Group(EntryPoint, dict):
|
|
|
31
35
|
return self
|
|
32
36
|
|
|
33
37
|
def __setitem__(self, key, value):
|
|
38
|
+
if isinstance(value, Group):
|
|
39
|
+
value.name = key
|
|
34
40
|
value.posix = self.posix
|
|
35
41
|
value.parent = self
|
|
36
42
|
value.outfile = self.outfile
|
|
@@ -54,7 +60,8 @@ class Group(EntryPoint, dict):
|
|
|
54
60
|
|
|
55
61
|
def group(self, name, **kwargs):
|
|
56
62
|
"""Return a new sub-group."""
|
|
57
|
-
|
|
63
|
+
kwargs.setdefault('cmdclass', self.cmdclass)
|
|
64
|
+
self[name] = Group(name, **kwargs)
|
|
58
65
|
return self[name]
|
|
59
66
|
|
|
60
67
|
def help(self, *args, error=None, short=False):
|