magicli 2.1.2__tar.gz → 2.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magicli
3
- Version: 2.1.2
3
+ Version: 2.1.3
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magicli
3
- Version: 2.1.2
3
+ Version: 2.1.3
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
@@ -73,8 +73,10 @@ def call(function, argv, module=None, name=None):
73
73
 
74
74
  try:
75
75
  args, kwargs = parse_argv(argv, parameters, docstring)
76
- except ParseArgvError:
77
- raise SystemExit(help_message(help_from_function, function, name))
76
+ except ParseArgvError as exc:
77
+ message = exc.args[0] + "\n\n" if exc.args else ""
78
+ message += help_message(help_from_function, function, name)
79
+ raise SystemExit(message)
78
80
 
79
81
  function(*args, **kwargs)
80
82
 
@@ -92,8 +94,8 @@ def parse_argv(argv, parameters, docstring):
92
94
  parse_short_options(key[1:], docstring, iter_argv, parameters, kwargs)
93
95
  else:
94
96
  if (index := len(args)) >= len(parameter_list):
95
- raise ParseArgvError
96
- args.append(get_type(parameter_list[index])(key))
97
+ raise ParseArgvError(f"{key}: unknown command")
98
+ args.append(cast_value(key, get_type(parameter_list[index])))
97
99
 
98
100
  check_all_args_present(len(args), parameter_list)
99
101
 
@@ -108,7 +110,9 @@ def check_all_args_present(len_args, parameter_list):
108
110
  if len_args < len(parameter_list):
109
111
  parameter = parameter_list[len_args]
110
112
  if parameter.default is parameter.empty:
111
- raise ParseArgvError
113
+ raise ParseArgvError(
114
+ f"{parameter_list[len_args].name}: positional argument missing"
115
+ )
112
116
 
113
117
 
114
118
  def parse_kwarg(key, argv, parameters):
@@ -119,16 +123,36 @@ def parse_kwarg(key, argv, parameters):
119
123
  """
120
124
  key, value = key.split("=", 1) if "=" in key else (key, None)
121
125
  key = key.replace("-", "_")
122
- cast_to = get_type(parameters.get(key))
126
+
127
+ if key not in parameters:
128
+ raise ParseArgvError(f"--{key}: unknown long option")
129
+
130
+ cast_to = get_type(parameters[key])
123
131
 
124
132
  if value is None:
125
133
  if cast_to is bool:
126
134
  return key, not parameters[key].default
127
135
  if cast_to is type(None):
128
136
  return key, True
129
- value = next(argv)
137
+ value = next_arg(argv)
138
+
139
+ return key, cast_value(value, cast_to)
140
+
130
141
 
131
- return key, value if cast_to is str else cast_to(value)
142
+ def next_arg(argv):
143
+ """Return the next command-line argument or raise a parser error."""
144
+ try:
145
+ return next(argv)
146
+ except StopIteration:
147
+ raise ParseArgvError("error: missing option value")
148
+
149
+
150
+ def cast_value(value, cast_to):
151
+ """Cast a command-line argument value or raise a parser error."""
152
+ try:
153
+ return value if cast_to is str else cast_to(value)
154
+ except ValueError as exc:
155
+ raise ParseArgvError(exc.args[0]) if exc.args else ParseArgvError from exc
132
156
 
133
157
 
134
158
  def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs):
@@ -137,7 +161,7 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
137
161
  long = short_to_long_option(short, docstring)
138
162
 
139
163
  if long not in parameters:
140
- raise SystemExit(f"--{long}: invalid long option")
164
+ raise ParseArgvError(f"--{long}: invalid long option")
141
165
 
142
166
  cast_to = get_type(parameters[long])
143
167
 
@@ -146,9 +170,9 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
146
170
  elif cast_to is type(None):
147
171
  kwargs[long] = True
148
172
  elif i == len(short_options) - 1:
149
- kwargs[long] = cast_to(next(iter_argv))
173
+ kwargs[long] = cast_value(next_arg(iter_argv), cast_to)
150
174
  else:
151
- raise SystemExit(f"-{short}: invalid type")
175
+ raise ParseArgvError(f"-{short}: invalid type")
152
176
 
153
177
 
154
178
  def short_to_long_option(short, docstring):
@@ -160,7 +184,7 @@ def short_to_long_option(short, docstring):
160
184
  chars = [" ", "\n", "]"]
161
185
  indices = (i for char in chars if (i := docstring.find(char, start)) != -1)
162
186
  return docstring[start : min(indices, default=None)]
163
- raise SystemExit(f"-{short}: invalid short option")
187
+ raise ParseArgvError(f"-{short}: invalid short option")
164
188
 
165
189
 
166
190
  def get_type(parameter):
@@ -59,8 +59,9 @@ def test_command_called(mocked, caplog):
59
59
  @mock.patch("importlib.import_module", side_effect=module)
60
60
  def test_wrong_command_not_called(mocked):
61
61
  sys.argv = ["name", "wrong_command"]
62
- with pytest.raises(SystemExit):
62
+ with pytest.raises(SystemExit) as error:
63
63
  magicli()
64
+ assert error.value.code.startswith("wrong_command: unknown command")
64
65
 
65
66
 
66
67
  @mock.patch("importlib.import_module", side_effect=module_empty)
@@ -87,8 +88,9 @@ def test_module_is_magicli(pyproject):
87
88
  @mock.patch("importlib.import_module", side_effect=module)
88
89
  def test_short_option_with_wrong_type(mocked):
89
90
  sys.argv = ["name", "-ab"]
90
- with pytest.raises(SystemExit):
91
+ with pytest.raises(SystemExit) as error:
91
92
  magicli()
93
+ assert error.value.code.startswith("-a: invalid short option")
92
94
 
93
95
 
94
96
  @mock.patch("importlib.import_module", side_effect=module_version)
@@ -103,7 +105,7 @@ def test_version(mocked, caplog):
103
105
  sys.argv = ["name", "-v"]
104
106
  with pytest.raises(SystemExit) as error:
105
107
  magicli()
106
- assert error.value.code == "-v: invalid short option"
108
+ assert error.value.code.startswith("-v: invalid short option")
107
109
 
108
110
 
109
111
  @mock.patch("importlib.import_module", side_effect=module_version)
@@ -3,7 +3,7 @@ from inspect import Parameter, _ParameterKind
3
3
 
4
4
  import pytest
5
5
 
6
- from magicli import get_type, parse_argv, parse_kwarg, ParseArgvError
6
+ from magicli import ParseArgvError, get_type, parse_argv, parse_kwarg
7
7
 
8
8
  PK = _ParameterKind.POSITIONAL_OR_KEYWORD
9
9
 
@@ -44,8 +44,6 @@ def test_parse_argv():
44
44
  ["a"],
45
45
  {"kwarg": 2},
46
46
  )
47
- with pytest.raises(ParseArgvError):
48
- parse_argv([], parameters, docstring="")
49
47
 
50
48
 
51
49
  def test_parse_argv_with_underscore():
@@ -58,3 +56,37 @@ def test_parse_argv_with_underscore():
58
56
  ["a"],
59
57
  {"kwarg_1": 2},
60
58
  )
59
+
60
+
61
+ @pytest.mark.parametrize(
62
+ ("command", "error_message"),
63
+ [
64
+ (["a", "--unknown=2"], "--unknown: unknown long option"),
65
+ (["a", "--kwarg"], "error: missing option value"),
66
+ ([], "arg: positional argument missing"),
67
+ ],
68
+ )
69
+ def test_parse_argv_errors(command, error_message):
70
+ parameters = inspect.signature(lambda arg, kwarg=1: None).parameters
71
+ with pytest.raises(ParseArgvError) as error:
72
+ parse_argv(command, parameters, docstring="")
73
+ assert error.value.args[0] == error_message
74
+
75
+
76
+ @pytest.mark.parametrize(
77
+ ("command", "result"),
78
+ [
79
+ (["--kwarg", "''"], "''"),
80
+ (["--kwarg="], ""),
81
+ ],
82
+ )
83
+ def test_parse_argv_empty_kwarg(command, result):
84
+ parameters = inspect.signature(lambda kwarg="1": None).parameters
85
+ res = parse_argv(command, parameters, docstring="")
86
+ assert res == ([], {"kwarg": result})
87
+
88
+
89
+ def test_parse_argv_with_invalid_type_raises_parse_error():
90
+ with pytest.raises(ParseArgvError) as error:
91
+ parse_argv(["not-an-int"], {"arg": Parameter("arg", PK, annotation=int)}, "")
92
+ assert error.value.args[0] == "invalid literal for int() with base 10: 'not-an-int'"
@@ -2,7 +2,7 @@ from inspect import Parameter, _ParameterKind
2
2
 
3
3
  import pytest
4
4
 
5
- from magicli import parse_short_options, short_to_long_option
5
+ from magicli import ParseArgvError, parse_short_options, short_to_long_option
6
6
 
7
7
 
8
8
  @pytest.mark.parametrize(
@@ -29,21 +29,21 @@ def test_parse_short_options(default, result):
29
29
 
30
30
 
31
31
  def test_parse_short_options_failures():
32
- _kwargs = {
32
+ kwargs = {
33
33
  "short_options": "a",
34
34
  "docstring": "-a, --abc",
35
35
  "iter_argv": iter(["b"]),
36
36
  "parameters": {"abc": Parameter("abc", _ParameterKind.KEYWORD_ONLY)},
37
37
  "kwargs": {},
38
38
  }
39
- for args in [
40
- {"parameters": {}},
41
- {"docstring": ""},
42
- {"short_options": "aa", "iter_argv": iter(["aa"])},
39
+ for args, err in [
40
+ ({"parameters": {}}, ("--abc: invalid long option",)),
41
+ ({"docstring": ""}, ("-a: invalid short option",)),
42
+ ({"short_options": "aa", "iter_argv": iter(["aa"])}, ("-a: invalid type",)),
43
43
  ]:
44
- with pytest.raises(SystemExit):
45
- parse_short_options(**(_kwargs | args))
46
- _kwargs["kwargs"] = {}
44
+ with pytest.raises(ParseArgvError) as error:
45
+ parse_short_options(**(kwargs | args))
46
+ assert error.value.args == err
47
47
 
48
48
 
49
49
  @pytest.mark.parametrize(
@@ -68,5 +68,6 @@ def test_short_to_long_option(docstring):
68
68
  ],
69
69
  )
70
70
  def test_short_to_long_option_failures(docstring):
71
- with pytest.raises(SystemExit):
71
+ with pytest.raises(ParseArgvError) as error:
72
72
  short_to_long_option("a", docstring)
73
+ assert error.value.args[0] == "-a: invalid short option"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes