magicli 2.1.4__tar.gz → 2.1.6__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.
- {magicli-2.1.4 → magicli-2.1.6}/PKG-INFO +24 -11
- {magicli-2.1.4 → magicli-2.1.6}/README.md +23 -10
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/PKG-INFO +24 -11
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/SOURCES.txt +2 -0
- magicli-2.1.6/magicli.egg-info/scm_file_list.json +19 -0
- magicli-2.1.6/magicli.egg-info/scm_version.json +8 -0
- {magicli-2.1.4 → magicli-2.1.6}/magicli.py +27 -25
- {magicli-2.1.4 → magicli-2.1.6}/tests/test_cli.py +0 -1
- {magicli-2.1.4 → magicli-2.1.6}/tests/test_magicli.py +26 -0
- {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/lint.yml +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/pytest.yml +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/release.yml +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/.gitignore +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/LICENSE +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/docs/img/hello.svg +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/dependency_links.txt +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/entry_points.txt +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/requires.txt +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/top_level.txt +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/pyproject.toml +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/setup.cfg +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/tests/fixtures.py +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/tests/test_help.py +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/tests/test_parse_kwarg.py +0 -0
- {magicli-2.1.4 → magicli-2.1.6}/tests/test_parse_short_options.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magicli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: Automatically generates a CLI from functions.
|
|
5
5
|
Author-email: Patrick Elmer <patrick@elmer.ws>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -13,9 +13,20 @@ Dynamic: license-file
|
|
|
13
13
|
|
|
14
14
|
# magiᴄʟɪ✨
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
magicli
|
|
18
|
+
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
automatically turns a file into a CLI by introspecting its functions.
|
|
21
|
+
|
|
22
|
+
- no boilerplate
|
|
23
|
+
- automatic help message
|
|
24
|
+
- type casting included
|
|
25
|
+
- supports sub-commands
|
|
26
|
+
- complete `pyproject.toml` setup
|
|
27
|
+
- ready for upload to `pypi`
|
|
28
|
+
|
|
29
|
+
Create a full CLI in only 2 lines of code:
|
|
19
30
|
|
|
20
31
|
```python
|
|
21
32
|
# hello.py
|
|
@@ -23,15 +34,11 @@ def hello(name, greeting="hello"):
|
|
|
23
34
|
print(greeting, name)
|
|
24
35
|
```
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<img src="docs/img/hello.svg"/>
|
|
37
|
+
calling `magicli` creates this CLI:
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+

|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
1. `magicli`
|
|
34
|
-
1. `pip install .`
|
|
41
|
+
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
35
42
|
|
|
36
43
|
## Quick start
|
|
37
44
|
|
|
@@ -69,12 +76,18 @@ _Make sure the name of your CLI, the module name and the name of the function ha
|
|
|
69
76
|
pip install .
|
|
70
77
|
```
|
|
71
78
|
|
|
79
|
+
or after uploading to `pypi`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install <package_name>
|
|
83
|
+
```
|
|
84
|
+
|
|
72
85
|
## Advanced use
|
|
73
86
|
|
|
74
87
|
### Using subcommands
|
|
75
88
|
|
|
76
89
|
```python
|
|
77
|
-
#
|
|
90
|
+
# hello.py
|
|
78
91
|
def hello(): ...
|
|
79
92
|
|
|
80
93
|
def world(times=1):
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# magiᴄʟɪ✨
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```bash
|
|
4
|
+
magicli
|
|
5
|
+
```
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
automatically turns a file into a CLI by introspecting its functions.
|
|
8
|
+
|
|
9
|
+
- no boilerplate
|
|
10
|
+
- automatic help message
|
|
11
|
+
- type casting included
|
|
12
|
+
- supports sub-commands
|
|
13
|
+
- complete `pyproject.toml` setup
|
|
14
|
+
- ready for upload to `pypi`
|
|
15
|
+
|
|
16
|
+
Create a full CLI in only 2 lines of code:
|
|
6
17
|
|
|
7
18
|
```python
|
|
8
19
|
# hello.py
|
|
@@ -10,15 +21,11 @@ def hello(name, greeting="hello"):
|
|
|
10
21
|
print(greeting, name)
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<img src="docs/img/hello.svg"/>
|
|
24
|
+
calling `magicli` creates this CLI:
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+

|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
1. `magicli`
|
|
21
|
-
1. `pip install .`
|
|
28
|
+
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
22
29
|
|
|
23
30
|
## Quick start
|
|
24
31
|
|
|
@@ -56,12 +63,18 @@ _Make sure the name of your CLI, the module name and the name of the function ha
|
|
|
56
63
|
pip install .
|
|
57
64
|
```
|
|
58
65
|
|
|
66
|
+
or after uploading to `pypi`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install <package_name>
|
|
70
|
+
```
|
|
71
|
+
|
|
59
72
|
## Advanced use
|
|
60
73
|
|
|
61
74
|
### Using subcommands
|
|
62
75
|
|
|
63
76
|
```python
|
|
64
|
-
#
|
|
77
|
+
# hello.py
|
|
65
78
|
def hello(): ...
|
|
66
79
|
|
|
67
80
|
def world(times=1):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magicli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: Automatically generates a CLI from functions.
|
|
5
5
|
Author-email: Patrick Elmer <patrick@elmer.ws>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -13,9 +13,20 @@ Dynamic: license-file
|
|
|
13
13
|
|
|
14
14
|
# magiᴄʟɪ✨
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
magicli
|
|
18
|
+
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
automatically turns a file into a CLI by introspecting its functions.
|
|
21
|
+
|
|
22
|
+
- no boilerplate
|
|
23
|
+
- automatic help message
|
|
24
|
+
- type casting included
|
|
25
|
+
- supports sub-commands
|
|
26
|
+
- complete `pyproject.toml` setup
|
|
27
|
+
- ready for upload to `pypi`
|
|
28
|
+
|
|
29
|
+
Create a full CLI in only 2 lines of code:
|
|
19
30
|
|
|
20
31
|
```python
|
|
21
32
|
# hello.py
|
|
@@ -23,15 +34,11 @@ def hello(name, greeting="hello"):
|
|
|
23
34
|
print(greeting, name)
|
|
24
35
|
```
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<img src="docs/img/hello.svg"/>
|
|
37
|
+
calling `magicli` creates this CLI:
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+

|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
1. `magicli`
|
|
34
|
-
1. `pip install .`
|
|
41
|
+
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
35
42
|
|
|
36
43
|
## Quick start
|
|
37
44
|
|
|
@@ -69,12 +76,18 @@ _Make sure the name of your CLI, the module name and the name of the function ha
|
|
|
69
76
|
pip install .
|
|
70
77
|
```
|
|
71
78
|
|
|
79
|
+
or after uploading to `pypi`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install <package_name>
|
|
83
|
+
```
|
|
84
|
+
|
|
72
85
|
## Advanced use
|
|
73
86
|
|
|
74
87
|
### Using subcommands
|
|
75
88
|
|
|
76
89
|
```python
|
|
77
|
-
#
|
|
90
|
+
# hello.py
|
|
78
91
|
def hello(): ...
|
|
79
92
|
|
|
80
93
|
def world(times=1):
|
|
@@ -12,6 +12,8 @@ magicli.egg-info/SOURCES.txt
|
|
|
12
12
|
magicli.egg-info/dependency_links.txt
|
|
13
13
|
magicli.egg-info/entry_points.txt
|
|
14
14
|
magicli.egg-info/requires.txt
|
|
15
|
+
magicli.egg-info/scm_file_list.json
|
|
16
|
+
magicli.egg-info/scm_version.json
|
|
15
17
|
magicli.egg-info/top_level.txt
|
|
16
18
|
tests/fixtures.py
|
|
17
19
|
tests/test_cli.py
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
"magicli.py",
|
|
7
|
+
".gitignore",
|
|
8
|
+
"docs/img/hello.svg",
|
|
9
|
+
"tests/fixtures.py",
|
|
10
|
+
"tests/test_magicli.py",
|
|
11
|
+
"tests/test_parse_kwarg.py",
|
|
12
|
+
"tests/test_parse_short_options.py",
|
|
13
|
+
"tests/test_help.py",
|
|
14
|
+
"tests/test_cli.py",
|
|
15
|
+
".github/workflows/release.yml",
|
|
16
|
+
".github/workflows/lint.yml",
|
|
17
|
+
".github/workflows/pytest.yml"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
`magicli` generates command-line interfaces from Python modules
|
|
3
3
|
by introspecting its functions and automatically parsing command-
|
|
4
4
|
line arguments based on function signatures.
|
|
5
5
|
"""
|
|
@@ -75,9 +75,9 @@ def call(function, argv, module=None, name=None):
|
|
|
75
75
|
try:
|
|
76
76
|
args, kwargs = parse_argv(argv, parameters, docstring)
|
|
77
77
|
except ParseArgvError as exc:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
raise SystemExit(
|
|
79
|
+
help_message(help_from_function, function, name, module, error=exc.args[0])
|
|
80
|
+
)
|
|
81
81
|
|
|
82
82
|
function(*args, **kwargs)
|
|
83
83
|
|
|
@@ -93,9 +93,9 @@ def parse_argv(argv, parameters, docstring):
|
|
|
93
93
|
kwargs[left] = right
|
|
94
94
|
elif key.startswith("-"):
|
|
95
95
|
parse_short_options(key[1:], docstring, iter_argv, parameters, kwargs)
|
|
96
|
+
elif (index := len(args)) >= len(parameter_list):
|
|
97
|
+
raise ParseArgvError(f"{key}: unknown command")
|
|
96
98
|
else:
|
|
97
|
-
if (index := len(args)) >= len(parameter_list):
|
|
98
|
-
raise ParseArgvError(f"{key}: unknown command")
|
|
99
99
|
args.append(cast_value(key, get_type(parameter_list[index])))
|
|
100
100
|
|
|
101
101
|
check_all_args_present(len(args), parameter_list)
|
|
@@ -125,14 +125,14 @@ def parse_kwarg(key, argv, parameters):
|
|
|
125
125
|
key, value = key.split("=", 1) if "=" in key else (key, None)
|
|
126
126
|
key = key.replace("-", "_")
|
|
127
127
|
|
|
128
|
-
if key
|
|
128
|
+
if (parameter := parameters.get(key)) is None:
|
|
129
129
|
raise ParseArgvError(f"--{key}: unknown long option")
|
|
130
130
|
|
|
131
|
-
cast_to = get_type(
|
|
131
|
+
cast_to = get_type(parameter)
|
|
132
132
|
|
|
133
133
|
if value is None:
|
|
134
134
|
if cast_to is bool:
|
|
135
|
-
return key, not
|
|
135
|
+
return key, not parameter.default
|
|
136
136
|
if cast_to is type(None):
|
|
137
137
|
return key, True
|
|
138
138
|
value = next_arg(argv)
|
|
@@ -161,13 +161,13 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
|
|
|
161
161
|
for i, short in enumerate(short_options):
|
|
162
162
|
long = short_to_long_option(short, docstring)
|
|
163
163
|
|
|
164
|
-
if long
|
|
164
|
+
if (parameter := parameters.get(long)) is None:
|
|
165
165
|
raise ParseArgvError(f"--{long}: invalid long option")
|
|
166
166
|
|
|
167
|
-
cast_to = get_type(
|
|
167
|
+
cast_to = get_type(parameter)
|
|
168
168
|
|
|
169
169
|
if cast_to is bool:
|
|
170
|
-
kwargs[long] = not
|
|
170
|
+
kwargs[long] = not parameter.default
|
|
171
171
|
elif cast_to is type(None):
|
|
172
172
|
kwargs[long] = True
|
|
173
173
|
elif i == len(short_options) - 1:
|
|
@@ -214,16 +214,17 @@ def check_for_version(argv, parameters, docstring, module):
|
|
|
214
214
|
raise SystemExit
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def help_message(help_function, obj, *args):
|
|
217
|
+
def help_message(help_function, obj, *args, error=None):
|
|
218
218
|
"""
|
|
219
219
|
Generates a help message for a function or module.
|
|
220
220
|
Returns the object's docstring if available, otherwise generates the help message
|
|
221
221
|
using the provided `help_function`.
|
|
222
222
|
"""
|
|
223
|
-
|
|
223
|
+
message = inspect.getdoc(obj) or help_function(obj, *args)
|
|
224
|
+
return (error + "\n\n" if error else "") + message if message else 1
|
|
224
225
|
|
|
225
226
|
|
|
226
|
-
def help_from_function(function, name=None):
|
|
227
|
+
def help_from_function(function, name=None, module=None):
|
|
227
228
|
"""
|
|
228
229
|
Generates a help message for a function based on its signature.
|
|
229
230
|
Displays the function name, required positional arguments, and
|
|
@@ -232,7 +233,11 @@ def help_from_function(function, name=None):
|
|
|
232
233
|
message = [name] if name else []
|
|
233
234
|
message.append(function.__name__)
|
|
234
235
|
message.extend(map(format_kwarg, inspect.signature(function).parameters.values()))
|
|
235
|
-
|
|
236
|
+
blocks = [["usage:", " ".join(message)]]
|
|
237
|
+
if module and (commands := get_commands(module)):
|
|
238
|
+
blocks[0].append(f"{module.__name__} <command>")
|
|
239
|
+
blocks.append(["commands:", *commands])
|
|
240
|
+
return format_blocks(blocks)
|
|
236
241
|
|
|
237
242
|
|
|
238
243
|
def format_kwarg(kwarg):
|
|
@@ -245,16 +250,11 @@ def help_from_module(module):
|
|
|
245
250
|
Generates a help message for a module and lists available commands.
|
|
246
251
|
Lists all public functions that are not excluded in `__all__`.
|
|
247
252
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if version := get_version(module):
|
|
251
|
-
blocks.append([f"{module.__name__} {version}"])
|
|
252
|
-
|
|
253
|
-
blocks.append(["usage:", f"{module.__name__} command"])
|
|
254
|
-
|
|
253
|
+
version = get_version(module)
|
|
254
|
+
blocks = [[f"{module.__name__} {version}"]] if version else []
|
|
255
255
|
if commands := get_commands(module):
|
|
256
|
+
blocks.append(["usage:", f"{module.__name__} command"])
|
|
256
257
|
blocks.append(["commands:", *commands])
|
|
257
|
-
|
|
258
258
|
return format_blocks(blocks)
|
|
259
259
|
|
|
260
260
|
|
|
@@ -276,7 +276,9 @@ def get_commands(module):
|
|
|
276
276
|
return [
|
|
277
277
|
name
|
|
278
278
|
for name, _ in inspect.getmembers(module, inspect.isfunction)
|
|
279
|
-
if not name.startswith("_")
|
|
279
|
+
if not name.startswith("_")
|
|
280
|
+
and name in module.__dict__.get("__all__", [name])
|
|
281
|
+
and name != module.__name__
|
|
280
282
|
]
|
|
281
283
|
|
|
282
284
|
|
|
@@ -33,6 +33,14 @@ module_version = partial(module, version="1.2.3")
|
|
|
33
33
|
module_empty = partial(create_module, functions=None)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def module_with_two_commands(name):
|
|
37
|
+
module = type(sys)(name)
|
|
38
|
+
setattr(module, "name", lambda: None)
|
|
39
|
+
setattr(module, "command", lambda: None)
|
|
40
|
+
setattr(module, "command_2", lambda: None)
|
|
41
|
+
return module
|
|
42
|
+
|
|
43
|
+
|
|
36
44
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
37
45
|
def test_module_imported(mocked):
|
|
38
46
|
sys.argv = ["name"]
|
|
@@ -115,3 +123,21 @@ def test_version_with_command(mocked, caplog):
|
|
|
115
123
|
magicli()
|
|
116
124
|
assert error.value.code is None
|
|
117
125
|
assert caplog.messages[0] == "1.2.3"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mock.patch("importlib.import_module", side_effect=module_with_two_commands)
|
|
129
|
+
def test_help_message_for_unknown_command(mocked):
|
|
130
|
+
sys.argv = ["name", "unknown_command"]
|
|
131
|
+
with pytest.raises(SystemExit) as error:
|
|
132
|
+
magicli()
|
|
133
|
+
assert error.value.code == """\
|
|
134
|
+
unknown_command: unknown command
|
|
135
|
+
|
|
136
|
+
usage:
|
|
137
|
+
<lambda>
|
|
138
|
+
name <command>
|
|
139
|
+
|
|
140
|
+
commands:
|
|
141
|
+
command
|
|
142
|
+
command_2\
|
|
143
|
+
"""
|
|
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
|