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.
Files changed (25) hide show
  1. {magicli-2.1.4 → magicli-2.1.6}/PKG-INFO +24 -11
  2. {magicli-2.1.4 → magicli-2.1.6}/README.md +23 -10
  3. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/PKG-INFO +24 -11
  4. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/SOURCES.txt +2 -0
  5. magicli-2.1.6/magicli.egg-info/scm_file_list.json +19 -0
  6. magicli-2.1.6/magicli.egg-info/scm_version.json +8 -0
  7. {magicli-2.1.4 → magicli-2.1.6}/magicli.py +27 -25
  8. {magicli-2.1.4 → magicli-2.1.6}/tests/test_cli.py +0 -1
  9. {magicli-2.1.4 → magicli-2.1.6}/tests/test_magicli.py +26 -0
  10. {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/lint.yml +0 -0
  11. {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/pytest.yml +0 -0
  12. {magicli-2.1.4 → magicli-2.1.6}/.github/workflows/release.yml +0 -0
  13. {magicli-2.1.4 → magicli-2.1.6}/.gitignore +0 -0
  14. {magicli-2.1.4 → magicli-2.1.6}/LICENSE +0 -0
  15. {magicli-2.1.4 → magicli-2.1.6}/docs/img/hello.svg +0 -0
  16. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/dependency_links.txt +0 -0
  17. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/entry_points.txt +0 -0
  18. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/requires.txt +0 -0
  19. {magicli-2.1.4 → magicli-2.1.6}/magicli.egg-info/top_level.txt +0 -0
  20. {magicli-2.1.4 → magicli-2.1.6}/pyproject.toml +0 -0
  21. {magicli-2.1.4 → magicli-2.1.6}/setup.cfg +0 -0
  22. {magicli-2.1.4 → magicli-2.1.6}/tests/fixtures.py +0 -0
  23. {magicli-2.1.4 → magicli-2.1.6}/tests/test_help.py +0 -0
  24. {magicli-2.1.4 → magicli-2.1.6}/tests/test_parse_kwarg.py +0 -0
  25. {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.4
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
- Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
16
+ ```bash
17
+ magicli
18
+ ```
17
19
 
18
- **`hello world` example**
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
- becomes
27
-
28
- <img src="docs/img/hello.svg"/>
37
+ calling `magicli` creates this CLI:
29
38
 
30
- with only 3 commands
39
+ ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/main/docs/img/hello.svg)
31
40
 
32
- 1. `pip install magicli`
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
- # module.py
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
- Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
3
+ ```bash
4
+ magicli
5
+ ```
4
6
 
5
- **`hello world` example**
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
- becomes
14
-
15
- <img src="docs/img/hello.svg"/>
24
+ calling `magicli` creates this CLI:
16
25
 
17
- with only 3 commands
26
+ ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/main/docs/img/hello.svg)
18
27
 
19
- 1. `pip install magicli`
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
- # module.py
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.4
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
- Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
16
+ ```bash
17
+ magicli
18
+ ```
17
19
 
18
- **`hello world` example**
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
- becomes
27
-
28
- <img src="docs/img/hello.svg"/>
37
+ calling `magicli` creates this CLI:
29
38
 
30
- with only 3 commands
39
+ ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/main/docs/img/hello.svg)
31
40
 
32
- 1. `pip install magicli`
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
- # module.py
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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.1.6",
3
+ "distance": 0,
4
+ "node": "g7e30827f02d07921c90e83b73547295a32bd9915",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-23"
8
+ }
@@ -1,5 +1,5 @@
1
1
  """
2
- Magicli generates command-line interfaces from Python modules
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
- message = exc.args[0] + "\n\n" if exc.args else ""
79
- message += help_message(help_from_function, function, name)
80
- raise SystemExit(message)
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 not in parameters:
128
+ if (parameter := parameters.get(key)) is None:
129
129
  raise ParseArgvError(f"--{key}: unknown long option")
130
130
 
131
- cast_to = get_type(parameters[key])
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 parameters[key].default
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 not in parameters:
164
+ if (parameter := parameters.get(long)) is None:
165
165
  raise ParseArgvError(f"--{long}: invalid long option")
166
166
 
167
- cast_to = get_type(parameters[long])
167
+ cast_to = get_type(parameter)
168
168
 
169
169
  if cast_to is bool:
170
- kwargs[long] = not parameters[long].default
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
- return inspect.getdoc(obj) or help_function(obj, *args) or 1
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
- return format_blocks([["usage:", " ".join(message)]])
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
- blocks = []
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("_") and name in module.__dict__.get("__all__", [name])
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
 
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from pathlib import Path
3
2
  from unittest import mock
4
3
 
@@ -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