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.
Files changed (25) hide show
  1. {magicli-2.2.0 → magicli-2.2.1}/.github/workflows/pytest.yml +2 -2
  2. {magicli-2.2.0 → magicli-2.2.1}/PKG-INFO +6 -4
  3. {magicli-2.2.0 → magicli-2.2.1}/README.md +3 -3
  4. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/PKG-INFO +6 -4
  5. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/SOURCES.txt +1 -1
  6. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/requires.txt +1 -0
  7. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/scm_file_list.json +10 -10
  8. magicli-2.2.1/magicli.egg-info/scm_version.json +8 -0
  9. {magicli-2.2.0 → magicli-2.2.1}/magicli.py +51 -42
  10. {magicli-2.2.0 → magicli-2.2.1}/pyproject.toml +2 -4
  11. {magicli-2.2.0 → magicli-2.2.1}/tests/test_cli.py +6 -15
  12. {magicli-2.2.0 → magicli-2.2.1}/tests/test_help.py +2 -0
  13. {magicli-2.2.0 → magicli-2.2.1}/tests/test_magicli.py +67 -44
  14. {magicli-2.2.0 → magicli-2.2.1}/tests/test_parse_short_options.py +1 -1
  15. magicli-2.2.0/magicli.egg-info/scm_version.json +0 -8
  16. {magicli-2.2.0 → magicli-2.2.1}/.github/workflows/lint.yml +0 -0
  17. {magicli-2.2.0 → magicli-2.2.1}/.github/workflows/release.yml +0 -0
  18. {magicli-2.2.0 → magicli-2.2.1}/.gitignore +0 -0
  19. {magicli-2.2.0 → magicli-2.2.1}/LICENSE +0 -0
  20. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/dependency_links.txt +0 -0
  21. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/entry_points.txt +0 -0
  22. {magicli-2.2.0 → magicli-2.2.1}/magicli.egg-info/top_level.txt +0 -0
  23. {magicli-2.2.0 → magicli-2.2.1}/setup.cfg +0 -0
  24. /magicli-2.2.0/tests/fixtures.py → /magicli-2.2.1/tests/conftest.py +0 -0
  25. {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: Run tests with pytest
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.0
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
  ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/assets/img/hello.svg)
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 to same name._
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, an error message will be printed.
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
  ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/assets/img/hello.svg)
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 to same name._
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, an error message will be printed.
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.0
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
  ![hello world example](https://raw.githubusercontent.com/PatrickElmer/magicli/refs/heads/assets/img/hello.svg)
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 to same name._
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, an error message will be printed.
110
+ If no docstring is specified, a usage message is printed.
109
111
 
110
112
  ## Development
111
113
 
@@ -14,7 +14,7 @@ magicli.egg-info/requires.txt
14
14
  magicli.egg-info/scm_file_list.json
15
15
  magicli.egg-info/scm_version.json
16
16
  magicli.egg-info/top_level.txt
17
- tests/fixtures.py
17
+ tests/conftest.py
18
18
  tests/test_cli.py
19
19
  tests/test_help.py
20
20
  tests/test_magicli.py
@@ -1,3 +1,4 @@
1
1
 
2
2
  [dev]
3
3
  pytest
4
+ pytest-cov
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "files": [
3
- "README.md",
3
+ ".github/workflows/lint.yml",
4
+ ".github/workflows/pytest.yml",
5
+ ".github/workflows/release.yml",
6
+ ".gitignore",
4
7
  "LICENSE",
5
- "pyproject.toml",
8
+ "README.md",
6
9
  "magicli.py",
7
- ".gitignore",
8
- "tests/fixtures.py",
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
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.2.1",
3
+ "distance": 0,
4
+ "node": "g6dcda982f974d7e3facd4f48e00084195018bee8",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-11-15"
8
+ }
@@ -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.__dict__.get(name)):
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 is_command(argv, module):
51
+ def is_public_function(module, name):
51
52
  """
52
- Checks if the first argument is a valid command in the module and returns
53
- the function to call if `argv[0]` is public and not excluded in `__all__`.
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
- if (
56
- argv
57
- and not (command := argv[0].replace("-", "_")).startswith("_")
58
- and command in module.__dict__.get("__all__", [command])
59
- and inspect.isfunction(function := module.__dict__.get(command))
60
- ):
61
- return function
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}: invalid type")
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
- logger.info(help_message(help_from_function, function, None, module))
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
- logger.info(get_version(module))
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, _ in inspect.getmembers(module, inspect.isfunction)
282
- if not name.startswith("_")
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.__dict__.get("__version__")
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
- try:
337
- module = importlib.import_module(name)
338
- except ModuleNotFoundError:
339
- return None
340
- doc = (module.__doc__ or "").split("\n\n")[0]
341
- return " ".join(stripped for line in doc.splitlines() if (stripped := line.strip()))
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
- logger.debug("Specify the version with `git tag`")
405
+ info("Specify the version with `git tag`")
397
406
  else:
398
- logger.debug("Not a git repo. Run `git init`")
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
- logger.debug("Unknown license: %s", license_file.name)
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
- logger.debug("Created pyproject.toml ✨")
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(caplog, with_git):
53
+ def test_with_git_repo(capsys, with_git):
63
54
  cli(name="_")
64
- assert "Specify the version with `git tag`" in caplog.messages
55
+ assert "Specify the version with `git tag`\n" in capsys.readouterr().err
65
56
 
66
57
 
67
- def test_without_git_repo(caplog, empty_directory):
58
+ def test_without_git_repo(capsys, empty_directory):
68
59
  cli(name="_")
69
- assert "Not a git repo. Run `git init`" in caplog.messages
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(caplog, with_readme_and_license):
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 any(msg == "Unknown license: LICENSE" for msg in caplog.messages)
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
- logging.info("name")
12
+ print("name")
15
13
 
16
14
 
17
15
  def command():
18
16
  "-v, --version"
19
- logging.info("command")
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.argv = ["name"]
47
- magicli()
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, caplog):
53
- sys.argv = ["name"]
54
- magicli()
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 caplog.messages[0] == "name"
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, caplog):
61
- sys.argv = ["name", "command"]
62
- magicli()
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 caplog.messages[0] == "command"
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
- sys.argv = ["name", "wrong_command"]
70
- with pytest.raises(SystemExit) as error:
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
- sys.argv = ["name"]
78
- with pytest.raises(SystemExit):
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
- sys.argv = ["_"]
84
- with pytest.raises(SystemExit):
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
- sys.argv = ["magicli"]
91
- with pytest.raises(SystemExit) as error:
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
- sys.argv = ["name", "-ab"]
99
- with pytest.raises(SystemExit) as error:
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, caplog):
115
+ def test_version(mocked, capsys):
106
116
  for version in ["--version", "-V"]:
107
- sys.argv = ["name", version]
108
- with pytest.raises(SystemExit) as error:
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 caplog.messages[0] == "1.2.3"
123
+ assert capsys.readouterr().out == "1.2.3\n"
112
124
 
113
- sys.argv = ["name", "-v"]
114
- with pytest.raises(SystemExit) as error:
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, caplog):
121
- sys.argv = ["name", "command", "-v"]
122
- with pytest.raises(SystemExit) as error:
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 caplog.messages[0] == "1.2.3"
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
- sys.argv = ["name", "unknown_command"]
131
- with pytest.raises(SystemExit) as error:
146
+ with (
147
+ mock.patch.object(sys, "argv", ["name", "unknown_command"]),
148
+ pytest.raises(SystemExit) as error,
149
+ ):
132
150
  magicli()
133
- assert error.value.code == """\
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, caplog):
147
- sys.argv = ["name", "--help"]
148
- name.__doc__ = None
149
- with pytest.raises(SystemExit):
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 caplog.messages[0] != name.__doc__
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: invalid type",)),
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))
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.2.0",
3
- "distance": 0,
4
- "node": "g9ecfd3ba03f51320894c356880a12e05ddf3f9cb",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-24"
8
- }
File without changes
File without changes
File without changes