magicli 2.2.0__tar.gz → 2.2.1__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.2.0 → magicli-2.2.1}/.github/workflows/pytest.yml +2 -2
- {magicli-2.2.0 → magicli-2.2.1}/PKG-INFO +6 -4
- {magicli-2.2.0 → magicli-2.2.1}/README.md +3 -3
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/PKG-INFO +6 -4
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/SOURCES.txt +1 -1
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/requires.txt +1 -0
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/scm_file_list.json +10 -10
- magicli-2.2.1/magicli.egg-info/scm_version.json +8 -0
- {magicli-2.2.0 → magicli-2.2.1}/magicli.py +51 -42
- {magicli-2.2.0 → magicli-2.2.1}/pyproject.toml +2 -4
- {magicli-2.2.0 → magicli-2.2.1}/tests/test_cli.py +6 -15
- {magicli-2.2.0 → magicli-2.2.1}/tests/test_help.py +2 -0
- {magicli-2.2.0 → magicli-2.2.1}/tests/test_magicli.py +67 -44
- {magicli-2.2.0 → magicli-2.2.1}/tests/test_parse_short_options.py +1 -1
- magicli-2.2.0/magicli.egg-info/scm_version.json +0 -8
- {magicli-2.2.0 → magicli-2.2.1}/.github/workflows/lint.yml +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/.github/workflows/release.yml +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/.gitignore +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/LICENSE +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/dependency_links.txt +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/entry_points.txt +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/top_level.txt +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/setup.cfg +0 -0
- /magicli-2.2.0/tests/fixtures.py → /magicli-2.2.1/tests/conftest.py +0 -0
- {magicli-2.2.0 → magicli-2.2.1}/tests/test_parse_kwarg.py +0 -0
|
@@ -8,11 +8,11 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
test:
|
|
11
|
-
name:
|
|
11
|
+
name: pytest
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
strategy:
|
|
14
14
|
matrix:
|
|
15
|
-
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
16
16
|
steps:
|
|
17
17
|
- uses: actions/checkout@v6
|
|
18
18
|
- uses: astral-sh/setup-uv@v8.1.0
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magicli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
7
7
|
Project-URL: Home, https://github.com/PatrickElmer/magicli
|
|
8
|
+
Requires-Python: >=3.10
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
License-File: LICENSE
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
12
14
|
Dynamic: license-file
|
|
13
15
|
|
|
14
16
|
# magiᴄʟɪ✨
|
|
@@ -38,7 +40,7 @@ calling `magicli` creates this CLI:
|
|
|
38
40
|
|
|
39
41
|

|
|
40
42
|
|
|
41
|
-
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
43
|
+
(animated terminal created with [clivio](https://patrickelmer.github.io/clivio))
|
|
42
44
|
|
|
43
45
|
## Quick start
|
|
44
46
|
|
|
@@ -68,7 +70,7 @@ git tag 1.0.0
|
|
|
68
70
|
magicli
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
_Make sure the name of your CLI, the module name and the name of the function have
|
|
73
|
+
_Make sure the name of your CLI, the module name and the name of the function have the same name._
|
|
72
74
|
|
|
73
75
|
### Install your Python package
|
|
74
76
|
|
|
@@ -105,7 +107,7 @@ hello world
|
|
|
105
107
|
|
|
106
108
|
By default, the docstring of the function will be displayed.
|
|
107
109
|
|
|
108
|
-
If no docstring is specified,
|
|
110
|
+
If no docstring is specified, a usage message is printed.
|
|
109
111
|
|
|
110
112
|
## Development
|
|
111
113
|
|
|
@@ -25,7 +25,7 @@ calling `magicli` creates this CLI:
|
|
|
25
25
|
|
|
26
26
|

|
|
27
27
|
|
|
28
|
-
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
28
|
+
(animated terminal created with [clivio](https://patrickelmer.github.io/clivio))
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
@@ -55,7 +55,7 @@ git tag 1.0.0
|
|
|
55
55
|
magicli
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
_Make sure the name of your CLI, the module name and the name of the function have
|
|
58
|
+
_Make sure the name of your CLI, the module name and the name of the function have the same name._
|
|
59
59
|
|
|
60
60
|
### Install your Python package
|
|
61
61
|
|
|
@@ -92,7 +92,7 @@ hello world
|
|
|
92
92
|
|
|
93
93
|
By default, the docstring of the function will be displayed.
|
|
94
94
|
|
|
95
|
-
If no docstring is specified,
|
|
95
|
+
If no docstring is specified, a usage message is printed.
|
|
96
96
|
|
|
97
97
|
## Development
|
|
98
98
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magicli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
7
7
|
Project-URL: Home, https://github.com/PatrickElmer/magicli
|
|
8
|
+
Requires-Python: >=3.10
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
License-File: LICENSE
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
12
14
|
Dynamic: license-file
|
|
13
15
|
|
|
14
16
|
# magiᴄʟɪ✨
|
|
@@ -38,7 +40,7 @@ calling `magicli` creates this CLI:
|
|
|
38
40
|
|
|
39
41
|

|
|
40
42
|
|
|
41
|
-
(animated terminal created with [clivio](patrickelmer.github.io/clivio))
|
|
43
|
+
(animated terminal created with [clivio](https://patrickelmer.github.io/clivio))
|
|
42
44
|
|
|
43
45
|
## Quick start
|
|
44
46
|
|
|
@@ -68,7 +70,7 @@ git tag 1.0.0
|
|
|
68
70
|
magicli
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
_Make sure the name of your CLI, the module name and the name of the function have
|
|
73
|
+
_Make sure the name of your CLI, the module name and the name of the function have the same name._
|
|
72
74
|
|
|
73
75
|
### Install your Python package
|
|
74
76
|
|
|
@@ -105,7 +107,7 @@ hello world
|
|
|
105
107
|
|
|
106
108
|
By default, the docstring of the function will be displayed.
|
|
107
109
|
|
|
108
|
-
If no docstring is specified,
|
|
110
|
+
If no docstring is specified, a usage message is printed.
|
|
109
111
|
|
|
110
112
|
## Development
|
|
111
113
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
-
"
|
|
3
|
+
".github/workflows/lint.yml",
|
|
4
|
+
".github/workflows/pytest.yml",
|
|
5
|
+
".github/workflows/release.yml",
|
|
6
|
+
".gitignore",
|
|
4
7
|
"LICENSE",
|
|
5
|
-
"
|
|
8
|
+
"README.md",
|
|
6
9
|
"magicli.py",
|
|
7
|
-
".
|
|
8
|
-
"tests/
|
|
10
|
+
"pyproject.toml",
|
|
11
|
+
"tests/conftest.py",
|
|
12
|
+
"tests/test_cli.py",
|
|
13
|
+
"tests/test_help.py",
|
|
9
14
|
"tests/test_magicli.py",
|
|
10
15
|
"tests/test_parse_kwarg.py",
|
|
11
|
-
"tests/test_parse_short_options.py"
|
|
12
|
-
"tests/test_help.py",
|
|
13
|
-
"tests/test_cli.py",
|
|
14
|
-
".github/workflows/release.yml",
|
|
15
|
-
".github/workflows/lint.yml",
|
|
16
|
-
".github/workflows/pytest.yml"
|
|
16
|
+
"tests/test_parse_short_options.py"
|
|
17
17
|
]
|
|
18
18
|
}
|
|
@@ -4,24 +4,25 @@ by introspecting its functions and automatically parsing command-
|
|
|
4
4
|
line arguments based on function signatures.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import ast
|
|
7
8
|
import importlib
|
|
8
9
|
import inspect
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
10
|
import subprocess
|
|
12
11
|
import sys
|
|
13
12
|
from functools import partial
|
|
14
13
|
from importlib import metadata
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
|
|
17
|
-
logging.basicConfig(level=os.getenv("MAGICLI_LOG_LEVEL", "DEBUG"), format="%(message)s")
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
16
|
|
|
21
17
|
class ParseArgvError(Exception):
|
|
22
18
|
"""Failure to parse argv into args and kwargs based on function parameters and docstring."""
|
|
23
19
|
|
|
24
20
|
|
|
21
|
+
def info(message):
|
|
22
|
+
"""Print a status message for the user to stderr."""
|
|
23
|
+
print(message, file=sys.stderr)
|
|
24
|
+
|
|
25
|
+
|
|
25
26
|
def magicli():
|
|
26
27
|
"""Parses command-line arguments and calls the appropriate function."""
|
|
27
28
|
name = Path(sys.argv[0]).name
|
|
@@ -30,7 +31,7 @@ def magicli():
|
|
|
30
31
|
if name == "magicli":
|
|
31
32
|
raise SystemExit(call(cli, argv, sys.modules["magicli"]))
|
|
32
33
|
|
|
33
|
-
module = load_module(name)
|
|
34
|
+
module = load_module(name.replace("-", "_"))
|
|
34
35
|
|
|
35
36
|
if function := get_function_from_argv(argv, module, name.replace("-", "_")):
|
|
36
37
|
function()
|
|
@@ -42,23 +43,28 @@ def get_function_from_argv(argv, module, name):
|
|
|
42
43
|
"""Returns the module's function to call based on argv."""
|
|
43
44
|
if function := is_command(argv, module):
|
|
44
45
|
return partial(call, function, argv[1:], module, name)
|
|
45
|
-
if inspect.isfunction(function := module
|
|
46
|
+
if inspect.isfunction(function := getattr(module, name, None)):
|
|
46
47
|
return partial(call, function, argv, module)
|
|
47
48
|
return None
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def
|
|
51
|
+
def is_public_function(module, name):
|
|
51
52
|
"""
|
|
52
|
-
Checks if
|
|
53
|
-
|
|
53
|
+
Checks if `name` is a public function of the module. With `__all__`, only listed
|
|
54
|
+
names are public. Without it, imported functions are excluded.
|
|
54
55
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
function = getattr(module, name, None)
|
|
57
|
+
if not inspect.isfunction(function) or name.startswith("_"):
|
|
58
|
+
return False
|
|
59
|
+
if hasattr(module, "__all__"):
|
|
60
|
+
return name in module.__all__
|
|
61
|
+
return function.__module__ == module.__name__
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_command(argv, module):
|
|
65
|
+
"""Returns the function named by `argv[0]` if it is a public command."""
|
|
66
|
+
if argv and is_public_function(module, command := argv[0].replace("-", "_")):
|
|
67
|
+
return getattr(module, command)
|
|
62
68
|
return None
|
|
63
69
|
|
|
64
70
|
|
|
@@ -68,7 +74,7 @@ def call(function, argv, module=None, name=None):
|
|
|
68
74
|
Displays a help message if an exception occurs.
|
|
69
75
|
"""
|
|
70
76
|
docstring = inspect.getdoc(function) or ""
|
|
71
|
-
parameters = inspect.signature(function).parameters
|
|
77
|
+
parameters = inspect.signature(function, eval_str=True).parameters
|
|
72
78
|
|
|
73
79
|
check_for_help_and_version(argv, parameters, docstring, module, function)
|
|
74
80
|
|
|
@@ -111,9 +117,7 @@ def check_all_args_present(len_args, parameter_list):
|
|
|
111
117
|
if len_args < len(parameter_list):
|
|
112
118
|
parameter = parameter_list[len_args]
|
|
113
119
|
if parameter.default is parameter.empty:
|
|
114
|
-
raise ParseArgvError(
|
|
115
|
-
f"{parameter_list[len_args].name}: positional argument missing"
|
|
116
|
-
)
|
|
120
|
+
raise ParseArgvError(f"{parameter.name}: positional argument missing")
|
|
117
121
|
|
|
118
122
|
|
|
119
123
|
def parse_kwarg(key, argv, parameters):
|
|
@@ -152,8 +156,8 @@ def cast_value(value, cast_to):
|
|
|
152
156
|
"""Cast a command-line argument value or raise a parser error."""
|
|
153
157
|
try:
|
|
154
158
|
return value if cast_to is str else cast_to(value)
|
|
155
|
-
except ValueError as exc:
|
|
156
|
-
raise ParseArgvError(exc.args[0]) if exc.args else ParseArgvError from exc
|
|
159
|
+
except (ValueError, TypeError) as exc:
|
|
160
|
+
raise ParseArgvError(exc.args[0]) if exc.args else ParseArgvError() from exc
|
|
157
161
|
|
|
158
162
|
|
|
159
163
|
def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs):
|
|
@@ -173,7 +177,7 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
|
|
|
173
177
|
elif i == len(short_options) - 1:
|
|
174
178
|
kwargs[long] = cast_value(next_arg(iter_argv), cast_to)
|
|
175
179
|
else:
|
|
176
|
-
raise ParseArgvError(f"-{short}:
|
|
180
|
+
raise ParseArgvError(f"-{short}: expected boolean")
|
|
177
181
|
|
|
178
182
|
|
|
179
183
|
def short_to_long_option(short, docstring):
|
|
@@ -184,7 +188,7 @@ def short_to_long_option(short, docstring):
|
|
|
184
188
|
if len(docstring) - start > 1:
|
|
185
189
|
chars = [" ", "\n", "]"]
|
|
186
190
|
indices = (i for char in chars if (i := docstring.find(char, start)) != -1)
|
|
187
|
-
return docstring[start : min(indices, default=None)]
|
|
191
|
+
return docstring[start : min(indices, default=None)].replace("-", "_")
|
|
188
192
|
raise ParseArgvError(f"-{short}: invalid short option")
|
|
189
193
|
|
|
190
194
|
|
|
@@ -205,7 +209,7 @@ def check_for_help_and_version(argv, parameters, docstring, module, function):
|
|
|
205
209
|
if not module or len(argv) != 1:
|
|
206
210
|
return
|
|
207
211
|
if argv[0] in ("--help", "-h") and "help" not in parameters:
|
|
208
|
-
|
|
212
|
+
print(help_message(help_from_function, function, None, module))
|
|
209
213
|
raise SystemExit
|
|
210
214
|
args = {
|
|
211
215
|
"--version": "--version",
|
|
@@ -213,7 +217,7 @@ def check_for_help_and_version(argv, parameters, docstring, module, function):
|
|
|
213
217
|
"-V": "-V, --version",
|
|
214
218
|
}
|
|
215
219
|
if (doc := args.get(argv[0])) and doc in docstring and "version" not in parameters:
|
|
216
|
-
|
|
220
|
+
print(get_version(module))
|
|
217
221
|
raise SystemExit
|
|
218
222
|
|
|
219
223
|
|
|
@@ -278,10 +282,8 @@ def get_commands(module):
|
|
|
278
282
|
"""Returns list of public commands that are not excluded by `__all__`."""
|
|
279
283
|
return [
|
|
280
284
|
name
|
|
281
|
-
for name
|
|
282
|
-
if
|
|
283
|
-
and name in module.__dict__.get("__all__", [name])
|
|
284
|
-
and name != module.__name__
|
|
285
|
+
for name in dir(module)
|
|
286
|
+
if name != module.__name__ and is_public_function(module, name)
|
|
285
287
|
]
|
|
286
288
|
|
|
287
289
|
|
|
@@ -290,7 +292,7 @@ def get_version(module):
|
|
|
290
292
|
try:
|
|
291
293
|
return metadata.version(module.__name__)
|
|
292
294
|
except metadata.PackageNotFoundError:
|
|
293
|
-
return module
|
|
295
|
+
return getattr(module, "__version__", None)
|
|
294
296
|
|
|
295
297
|
|
|
296
298
|
def get_project_name():
|
|
@@ -333,12 +335,19 @@ def get_homepage(url=None):
|
|
|
333
335
|
|
|
334
336
|
def get_description(name):
|
|
335
337
|
"""Return the first paragraph of a module's docstring if available."""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
for path in (
|
|
339
|
+
Path(f"{name}.py"),
|
|
340
|
+
Path(name, "__init__.py"),
|
|
341
|
+
Path("src", name, "__init__.py"),
|
|
342
|
+
):
|
|
343
|
+
if path.is_file():
|
|
344
|
+
try:
|
|
345
|
+
tree = ast.parse(path.read_bytes())
|
|
346
|
+
except (SyntaxError, ValueError):
|
|
347
|
+
return None
|
|
348
|
+
doc = ast.get_docstring(tree) or ""
|
|
349
|
+
return " ".join(doc.split("\n\n")[0].split()) or None
|
|
350
|
+
return None
|
|
342
351
|
|
|
343
352
|
|
|
344
353
|
def get_license_expression(content):
|
|
@@ -393,9 +402,9 @@ def cli(name="", author="", email="", description="", homepage=""):
|
|
|
393
402
|
author = author or get_output("git config --get user.name")
|
|
394
403
|
email = email or get_output("git config --get user.email")
|
|
395
404
|
if not get_output("git tag"):
|
|
396
|
-
|
|
405
|
+
info("Specify the version with `git tag`")
|
|
397
406
|
else:
|
|
398
|
-
|
|
407
|
+
info("Not a git repo. Run `git init`")
|
|
399
408
|
|
|
400
409
|
authors = [f'{k}="{v}"' for k, v in {"name": author, "email": email}.items() if v]
|
|
401
410
|
|
|
@@ -417,7 +426,7 @@ def cli(name="", author="", email="", description="", homepage=""):
|
|
|
417
426
|
if license_expression := get_license_expression(license_content):
|
|
418
427
|
project.append(f'license = "{license_expression}"')
|
|
419
428
|
else:
|
|
420
|
-
|
|
429
|
+
info(f"Unknown license: {license_file.name}")
|
|
421
430
|
project.append(f'license-files = ["{license_file.name}"]')
|
|
422
431
|
|
|
423
432
|
if description or (description := get_description(name)):
|
|
@@ -437,4 +446,4 @@ def cli(name="", author="", email="", description="", homepage=""):
|
|
|
437
446
|
)
|
|
438
447
|
|
|
439
448
|
pyproject.write_text(format_blocks(blocks, sep="\n") + "\n", encoding="utf-8")
|
|
440
|
-
|
|
449
|
+
info("Created pyproject.toml ✨")
|
|
@@ -9,13 +9,14 @@ magicli = "magicli:magicli"
|
|
|
9
9
|
name = "magicli"
|
|
10
10
|
authors = [{name = "Patrick Elmer", email = "patrick@elmer.ws"}]
|
|
11
11
|
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
12
13
|
description = "Automatically generates a CLI from functions."
|
|
13
14
|
dynamic = ["version"]
|
|
14
15
|
license = "GPL-3.0-or-later"
|
|
15
16
|
license-files = ["LICENSE"]
|
|
16
17
|
|
|
17
18
|
[project.optional-dependencies]
|
|
18
|
-
dev = ["pytest"]
|
|
19
|
+
dev = ["pytest", "pytest-cov"]
|
|
19
20
|
|
|
20
21
|
[project.urls]
|
|
21
22
|
Home = "https://github.com/PatrickElmer/magicli"
|
|
@@ -25,6 +26,3 @@ disable = [
|
|
|
25
26
|
"unidiomatic-typecheck",
|
|
26
27
|
"raise-missing-from",
|
|
27
28
|
]
|
|
28
|
-
|
|
29
|
-
[tool.pytest]
|
|
30
|
-
log_level = "DEBUG"
|
|
@@ -2,15 +2,6 @@ from pathlib import Path
|
|
|
2
2
|
from unittest import mock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from fixtures import (
|
|
6
|
-
empty_directory,
|
|
7
|
-
pyproject,
|
|
8
|
-
with_git,
|
|
9
|
-
with_license,
|
|
10
|
-
with_readme_and_license,
|
|
11
|
-
with_tempdir,
|
|
12
|
-
with_two_files,
|
|
13
|
-
)
|
|
14
5
|
|
|
15
6
|
from magicli import (
|
|
16
7
|
cli,
|
|
@@ -59,14 +50,14 @@ def test_empty_cli_name_failure(with_two_files):
|
|
|
59
50
|
assert error.value.code == 1
|
|
60
51
|
|
|
61
52
|
|
|
62
|
-
def test_with_git_repo(
|
|
53
|
+
def test_with_git_repo(capsys, with_git):
|
|
63
54
|
cli(name="_")
|
|
64
|
-
assert "Specify the version with `git tag
|
|
55
|
+
assert "Specify the version with `git tag`\n" in capsys.readouterr().err
|
|
65
56
|
|
|
66
57
|
|
|
67
|
-
def test_without_git_repo(
|
|
58
|
+
def test_without_git_repo(capsys, empty_directory):
|
|
68
59
|
cli(name="_")
|
|
69
|
-
assert "Not a git repo. Run `git init
|
|
60
|
+
assert "Not a git repo. Run `git init`\n" in capsys.readouterr().err
|
|
70
61
|
|
|
71
62
|
|
|
72
63
|
def test_get_output():
|
|
@@ -100,7 +91,7 @@ def test_cli_with_license(with_license):
|
|
|
100
91
|
assert 'license-files = ["LICENSE"]' in pyproject
|
|
101
92
|
|
|
102
93
|
|
|
103
|
-
def test_cli_with_kwargs(
|
|
94
|
+
def test_cli_with_kwargs(capsys, with_readme_and_license):
|
|
104
95
|
cli(
|
|
105
96
|
name="name",
|
|
106
97
|
author="Patrick Elmer",
|
|
@@ -108,7 +99,7 @@ def test_cli_with_kwargs(caplog, with_readme_and_license):
|
|
|
108
99
|
description="docstring",
|
|
109
100
|
homepage="https://github.com/PatrickElmer/magicli",
|
|
110
101
|
)
|
|
111
|
-
assert
|
|
102
|
+
assert "Unknown license: LICENSE\n" in capsys.readouterr().err
|
|
112
103
|
assert (
|
|
113
104
|
Path("pyproject.toml").read_text(encoding="utf-8")
|
|
114
105
|
== """\
|
|
@@ -17,6 +17,7 @@ def test_help_from_function_with_name():
|
|
|
17
17
|
def test_help_from_module():
|
|
18
18
|
module = type(sys)("name")
|
|
19
19
|
module.command = f1
|
|
20
|
+
module.__all__ = ["command"]
|
|
20
21
|
assert (
|
|
21
22
|
help_from_module(module)
|
|
22
23
|
== """\
|
|
@@ -33,6 +34,7 @@ def test_help_from_module_with_version():
|
|
|
33
34
|
module = type(sys)("name")
|
|
34
35
|
module.__dict__["__version__"] = "0.1.2"
|
|
35
36
|
module.command = f1
|
|
37
|
+
module.__all__ = ["command"]
|
|
36
38
|
assert (
|
|
37
39
|
help_from_module(module)
|
|
38
40
|
== """\
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import sys
|
|
3
2
|
from functools import partial
|
|
4
3
|
from unittest import mock
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
7
|
-
from fixtures import pyproject
|
|
8
6
|
|
|
9
7
|
from magicli import magicli
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
def name():
|
|
13
11
|
"-V, --version"
|
|
14
|
-
|
|
12
|
+
print("name")
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def command():
|
|
18
16
|
"-v, --version"
|
|
19
|
-
|
|
17
|
+
print("command")
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
def create_module(name, version=None, functions=None):
|
|
@@ -25,6 +23,7 @@ def create_module(name, version=None, functions=None):
|
|
|
25
23
|
module.__version__ = version
|
|
26
24
|
for function in functions or []:
|
|
27
25
|
setattr(module, function.__name__, function)
|
|
26
|
+
module.__all__ = [function.__name__ for function in functions or []]
|
|
28
27
|
return module
|
|
29
28
|
|
|
30
29
|
|
|
@@ -38,99 +37,120 @@ def module_with_two_commands(name):
|
|
|
38
37
|
setattr(module, "name", lambda: None)
|
|
39
38
|
setattr(module, "command", lambda: None)
|
|
40
39
|
setattr(module, "command_2", lambda: None)
|
|
40
|
+
module.__all__ = ["name", "command", "command_2"]
|
|
41
41
|
return module
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
45
45
|
def test_module_imported(mocked):
|
|
46
|
-
sys
|
|
47
|
-
|
|
46
|
+
with mock.patch.object(sys, "argv", ["name"]):
|
|
47
|
+
magicli()
|
|
48
48
|
mocked.assert_called_once_with("name")
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
52
|
-
def test_first_function_called(mocked,
|
|
53
|
-
sys
|
|
54
|
-
|
|
52
|
+
def test_first_function_called(mocked, capsys):
|
|
53
|
+
with mock.patch.object(sys, "argv", ["name"]):
|
|
54
|
+
magicli()
|
|
55
55
|
mocked.assert_called_once_with("name")
|
|
56
|
-
assert
|
|
56
|
+
assert capsys.readouterr().out == "name\n"
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
60
|
-
def test_command_called(mocked,
|
|
61
|
-
sys
|
|
62
|
-
|
|
60
|
+
def test_command_called(mocked, capsys):
|
|
61
|
+
with mock.patch.object(sys, "argv", ["name", "command"]):
|
|
62
|
+
magicli()
|
|
63
63
|
mocked.assert_called_once_with("name")
|
|
64
|
-
assert
|
|
64
|
+
assert capsys.readouterr().out == "command\n"
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
68
68
|
def test_wrong_command_not_called(mocked):
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
with (
|
|
70
|
+
mock.patch.object(sys, "argv", ["name", "wrong_command"]),
|
|
71
|
+
pytest.raises(SystemExit) as error,
|
|
72
|
+
):
|
|
71
73
|
magicli()
|
|
72
74
|
assert error.value.code.startswith("wrong_command: unknown command")
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
@mock.patch("importlib.import_module", side_effect=module_empty)
|
|
76
78
|
def test_module_without_functions(mocked):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
with (
|
|
80
|
+
mock.patch.object(sys, "argv", ["name"]),
|
|
81
|
+
pytest.raises(SystemExit),
|
|
82
|
+
):
|
|
79
83
|
magicli()
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
def test_module_not_found():
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
with (
|
|
88
|
+
mock.patch.object(sys, "argv", ["_"]),
|
|
89
|
+
pytest.raises(SystemExit),
|
|
90
|
+
):
|
|
85
91
|
magicli()
|
|
86
92
|
|
|
87
93
|
|
|
88
94
|
@mock.patch("builtins.input", return_value="n")
|
|
89
|
-
def test_module_is_magicli(pyproject):
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
def test_module_is_magicli(_, pyproject):
|
|
96
|
+
with (
|
|
97
|
+
mock.patch.object(sys, "argv", ["magicli"]),
|
|
98
|
+
pytest.raises(SystemExit) as error,
|
|
99
|
+
):
|
|
92
100
|
magicli()
|
|
93
101
|
assert error.value.code == 1
|
|
94
102
|
|
|
95
103
|
|
|
96
104
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
97
105
|
def test_short_option_with_wrong_type(mocked):
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
with (
|
|
107
|
+
mock.patch.object(sys, "argv", ["name", "-ab"]),
|
|
108
|
+
pytest.raises(SystemExit) as error,
|
|
109
|
+
):
|
|
100
110
|
magicli()
|
|
101
111
|
assert error.value.code.startswith("-a: invalid short option")
|
|
102
112
|
|
|
103
113
|
|
|
104
114
|
@mock.patch("importlib.import_module", side_effect=module_version)
|
|
105
|
-
def test_version(mocked,
|
|
115
|
+
def test_version(mocked, capsys):
|
|
106
116
|
for version in ["--version", "-V"]:
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
with (
|
|
118
|
+
mock.patch.object(sys, "argv", ["name", version]),
|
|
119
|
+
pytest.raises(SystemExit) as error,
|
|
120
|
+
):
|
|
109
121
|
magicli()
|
|
110
122
|
assert error.value.code is None
|
|
111
|
-
assert
|
|
123
|
+
assert capsys.readouterr().out == "1.2.3\n"
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
with (
|
|
126
|
+
mock.patch.object(sys, "argv", ["name", "-v"]),
|
|
127
|
+
pytest.raises(SystemExit) as error,
|
|
128
|
+
):
|
|
115
129
|
magicli()
|
|
116
130
|
assert error.value.code.startswith("-v: invalid short option")
|
|
117
131
|
|
|
118
132
|
|
|
119
133
|
@mock.patch("importlib.import_module", side_effect=module_version)
|
|
120
|
-
def test_version_with_command(mocked,
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
def test_version_with_command(mocked, capsys):
|
|
135
|
+
with (
|
|
136
|
+
mock.patch.object(sys, "argv", ["name", "command", "-v"]),
|
|
137
|
+
pytest.raises(SystemExit) as error,
|
|
138
|
+
):
|
|
123
139
|
magicli()
|
|
124
140
|
assert error.value.code is None
|
|
125
|
-
assert
|
|
141
|
+
assert capsys.readouterr().out == "1.2.3\n"
|
|
126
142
|
|
|
127
143
|
|
|
128
144
|
@mock.patch("importlib.import_module", side_effect=module_with_two_commands)
|
|
129
145
|
def test_help_message_for_unknown_command(mocked):
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
with (
|
|
147
|
+
mock.patch.object(sys, "argv", ["name", "unknown_command"]),
|
|
148
|
+
pytest.raises(SystemExit) as error,
|
|
149
|
+
):
|
|
132
150
|
magicli()
|
|
133
|
-
assert
|
|
151
|
+
assert (
|
|
152
|
+
error.value.code
|
|
153
|
+
== """\
|
|
134
154
|
unknown_command: unknown command
|
|
135
155
|
|
|
136
156
|
usage:
|
|
@@ -141,12 +161,16 @@ commands:
|
|
|
141
161
|
command
|
|
142
162
|
command_2\
|
|
143
163
|
"""
|
|
164
|
+
)
|
|
165
|
+
|
|
144
166
|
|
|
145
167
|
@mock.patch("importlib.import_module", side_effect=module)
|
|
146
|
-
def test_help_kwarg_displays_help_message(mocked,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
def test_help_kwarg_displays_help_message(mocked, capsys, monkeypatch):
|
|
169
|
+
monkeypatch.setattr(name, "__doc__", None)
|
|
170
|
+
with (
|
|
171
|
+
mock.patch.object(sys, "argv", ["name", "--help"]),
|
|
172
|
+
pytest.raises(SystemExit),
|
|
173
|
+
):
|
|
150
174
|
magicli()
|
|
151
175
|
doc = """usage:
|
|
152
176
|
name
|
|
@@ -154,5 +178,4 @@ def test_help_kwarg_displays_help_message(mocked, caplog):
|
|
|
154
178
|
|
|
155
179
|
commands:
|
|
156
180
|
command"""
|
|
157
|
-
assert
|
|
158
|
-
assert caplog.messages[0] == doc
|
|
181
|
+
assert capsys.readouterr().out == doc + "\n"
|
|
@@ -39,7 +39,7 @@ def test_parse_short_options_failures():
|
|
|
39
39
|
for args, err in [
|
|
40
40
|
({"parameters": {}}, ("--abc: invalid long option",)),
|
|
41
41
|
({"docstring": ""}, ("-a: invalid short option",)),
|
|
42
|
-
({"short_options": "aa", "iter_argv": iter(["aa"])}, ("-a:
|
|
42
|
+
({"short_options": "aa", "iter_argv": iter(["aa"])}, ("-a: expected boolean",)),
|
|
43
43
|
]:
|
|
44
44
|
with pytest.raises(ParseArgvError) as error:
|
|
45
45
|
parse_short_options(**(kwargs | args))
|
|
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
|