magicli 2.1.2__tar.gz → 2.1.4__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,7 @@
1
1
  name: Lint
2
2
 
3
3
  on:
4
+ workflow_dispatch:
4
5
  pull_request:
5
6
  paths:
6
7
  - magicli.py
@@ -9,8 +10,8 @@ jobs:
9
10
  flake8:
10
11
  runs-on: ubuntu-latest
11
12
  steps:
12
- - uses: actions/checkout@v4
13
- - uses: astral-sh/setup-uv@v5
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v8.1.0
14
15
  with:
15
16
  cache-dependency-glob: ""
16
17
  - run: uv run --with flake8 flake8 magicli.py --extend-ignore=E203,E501
@@ -18,8 +19,8 @@ jobs:
18
19
  pylint:
19
20
  runs-on: ubuntu-latest
20
21
  steps:
21
- - uses: actions/checkout@v4
22
- - uses: astral-sh/setup-uv@v5
22
+ - uses: actions/checkout@v6
23
+ - uses: astral-sh/setup-uv@v8.1.0
23
24
  with:
24
25
  cache-dependency-glob: ""
25
26
  - run: uv run --with pylint pylint magicli.py
@@ -27,8 +28,8 @@ jobs:
27
28
  ruff:
28
29
  runs-on: ubuntu-latest
29
30
  steps:
30
- - uses: actions/checkout@v4
31
- - uses: astral-sh/setup-uv@v5
31
+ - uses: actions/checkout@v6
32
+ - uses: astral-sh/setup-uv@v8.1.0
32
33
  with:
33
34
  cache-dependency-glob: ""
34
35
  - run: uv run --with ruff ruff format magicli.py --diff
@@ -36,8 +37,8 @@ jobs:
36
37
  isort:
37
38
  runs-on: ubuntu-latest
38
39
  steps:
39
- - uses: actions/checkout@v4
40
- - uses: astral-sh/setup-uv@v5
40
+ - uses: actions/checkout@v6
41
+ - uses: astral-sh/setup-uv@v8.1.0
41
42
  with:
42
43
  cache-dependency-glob: ""
43
44
  - run: uv run --with isort isort --check --diff magicli.py
@@ -1,6 +1,7 @@
1
1
  name: Pytest
2
2
 
3
3
  on:
4
+ workflow_dispatch:
4
5
  pull_request:
5
6
  paths:
6
7
  - "**/*.py"
@@ -13,8 +14,8 @@ jobs:
13
14
  matrix:
14
15
  python-version: ["3.10", "3.11", "3.12", "3.13"]
15
16
  steps:
16
- - uses: actions/checkout@v4
17
- - uses: astral-sh/setup-uv@v5
17
+ - uses: actions/checkout@v6
18
+ - uses: astral-sh/setup-uv@v8.1.0
18
19
  with:
19
20
  cache-dependency-glob: ""
20
21
  python-version: ${{ matrix.python-version }}
@@ -14,7 +14,7 @@ jobs:
14
14
  permissions:
15
15
  id-token: write
16
16
  steps:
17
- - uses: actions/checkout@v4
18
- - uses: astral-sh/setup-uv@v3
17
+ - uses: actions/checkout@v6
18
+ - uses: astral-sh/setup-uv@v8.1.0
19
19
  - run: uv build
20
20
  - run: uv publish --trusted-publishing always
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magicli
3
- Version: 2.1.2
3
+ Version: 2.1.4
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,70 +13,64 @@ Dynamic: license-file
13
13
 
14
14
  # magiᴄʟɪ✨
15
15
 
16
- Automatically generates a CLI from functions of a module.
16
+ Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
17
17
 
18
- ## Install
18
+ **`hello world` example**
19
19
 
20
- ```
21
- pip install magicli
20
+ ```python
21
+ # hello.py
22
+ def hello(name, greeting="hello"):
23
+ print(greeting, name)
22
24
  ```
23
25
 
24
- ## Get started
26
+ becomes
25
27
 
26
- Simple usage example.
28
+ <img src="docs/img/hello.svg"/>
27
29
 
28
- ```python
29
- # module.py
30
- def hello(name, times=1):
31
- for _ in range(times):
32
- print("hello", name)
33
- ```
30
+ with only 3 commands
34
31
 
35
- Then run this in your terminal to initialize the CLI:
32
+ 1. `pip install magicli`
33
+ 1. `magicli`
34
+ 1. `pip install .`
36
35
 
37
- ```bash
38
- magicli
39
- ```
36
+ ## Quick start
40
37
 
41
- You are now able to install your package:
38
+ ### Install magicli
42
39
 
43
- ```bash
44
- pip install .
40
+ ```
41
+ pip install magicli
45
42
  ```
46
43
 
47
- And then call it:
44
+ ### Setup your repository
45
+
46
+ Initialize repo
48
47
 
49
48
  ```bash
50
- $ hello world --times 2
51
- hello world
52
- hello world
49
+ git init
53
50
  ```
54
51
 
55
- ### Define name of CLI in `pyproject.toml`
52
+ Add version info (optional)
56
53
 
57
- The terminal command `magicli` adds the following to your `pyproject.toml`.
58
-
59
- ```toml
60
- [project.scripts]
61
- hello = "magicli:magicli"
54
+ ```bash
55
+ git tag 1.0.0
62
56
  ```
63
57
 
64
- Important: Make sure the name of your CLI, the module name and the name of the function to be called are the same!
58
+ ### Automatically create CLI
65
59
 
66
60
  ```bash
67
- .
68
- ├── hello.py
69
- └── pyproject.toml
61
+ magicli
70
62
  ```
71
63
 
72
- You can now `pip install` your code and call it like this:
64
+ _Make sure the name of your CLI, the module name and the name of the function have to same name._
65
+
66
+ ### Install your Python package
73
67
 
74
68
  ```bash
75
- $ hello world --times 2
76
- hello world
77
- hello world
69
+ pip install .
78
70
  ```
79
71
 
72
+ ## Advanced use
73
+
80
74
  ### Using subcommands
81
75
 
82
76
  ```python
@@ -0,0 +1,90 @@
1
+ # magiᴄʟɪ✨
2
+
3
+ Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
4
+
5
+ **`hello world` example**
6
+
7
+ ```python
8
+ # hello.py
9
+ def hello(name, greeting="hello"):
10
+ print(greeting, name)
11
+ ```
12
+
13
+ becomes
14
+
15
+ <img src="docs/img/hello.svg"/>
16
+
17
+ with only 3 commands
18
+
19
+ 1. `pip install magicli`
20
+ 1. `magicli`
21
+ 1. `pip install .`
22
+
23
+ ## Quick start
24
+
25
+ ### Install magicli
26
+
27
+ ```
28
+ pip install magicli
29
+ ```
30
+
31
+ ### Setup your repository
32
+
33
+ Initialize repo
34
+
35
+ ```bash
36
+ git init
37
+ ```
38
+
39
+ Add version info (optional)
40
+
41
+ ```bash
42
+ git tag 1.0.0
43
+ ```
44
+
45
+ ### Automatically create CLI
46
+
47
+ ```bash
48
+ magicli
49
+ ```
50
+
51
+ _Make sure the name of your CLI, the module name and the name of the function have to same name._
52
+
53
+ ### Install your Python package
54
+
55
+ ```bash
56
+ pip install .
57
+ ```
58
+
59
+ ## Advanced use
60
+
61
+ ### Using subcommands
62
+
63
+ ```python
64
+ # module.py
65
+ def hello(): ...
66
+
67
+ def world(times=1):
68
+ for _ in range(times):
69
+ print("hello world")
70
+ ```
71
+
72
+ ```bash
73
+ $ hello world --times 2
74
+ hello world
75
+ hello world
76
+ ```
77
+
78
+ ### Help message
79
+
80
+ By default, the docstring of the function will be displayed.
81
+
82
+ If no docstring is specified, an error message will be printed.
83
+
84
+ ## Development
85
+
86
+ Run pytest with coverage report:
87
+
88
+ ```bash
89
+ python3 -m pytest -s --cov=magicli --cov-report=term-missing
90
+ ```
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="540" height="222" font-family="monospace" font-size="18px" fill="#ffffff"><rect width="100%" height="100%" rx="10" fill="#151b23" /><g transform="scale(0.9) translate(20 38)"><text y="0" opacity="0" style="user-select: none;">$ <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0;0;1;1" dur="11s" repeatCount="indefinite" /></text><text x="21.6" y="0" opacity="0">h<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.0545;0.0545;1;1" dur="11s" repeatCount="indefinite" /></text><text x="32.400000000000006" y="0" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.0636;0.0636;1;1" dur="11s" repeatCount="indefinite" /></text><text x="43.2" y="0" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.0727;0.0727;1;1" dur="11s" repeatCount="indefinite" /></text><text x="54" y="0" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.0818;0.0818;1;1" dur="11s" repeatCount="indefinite" /></text><text x="64.80000000000001" y="0" opacity="0">o<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.0909;0.0909;1;1" dur="11s" repeatCount="indefinite" /></text><text x="75.60000000000001" y="0" opacity="0"> <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1;0.1;1;1" dur="11s" repeatCount="indefinite" /></text><text x="86.4" y="0" opacity="0">w<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1091;0.1091;1;1" dur="11s" repeatCount="indefinite" /></text><text x="97.2" y="0" opacity="0">o<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1182;0.1182;1;1" dur="11s" repeatCount="indefinite" /></text><text x="108" y="0" opacity="0">r<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1273;0.1273;1;1" dur="11s" repeatCount="indefinite" /></text><text x="118.80000000000001" y="0" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1364;0.1364;1;1" dur="11s" repeatCount="indefinite" /></text><text x="129.60000000000002" y="0" opacity="0">d<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1455;0.1455;1;1" dur="11s" repeatCount="indefinite" /></text><text y="23" opacity="0">hello&#160;world<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1545;0.1545;1;1" dur="11s" repeatCount="indefinite" /></text><text y="46" opacity="0" style="user-select: none;">$ <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.1545;0.1545;1;1" dur="11s" repeatCount="indefinite" /></text><text x="21.6" y="46" opacity="0">h<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3455;0.3455;1;1" dur="11s" repeatCount="indefinite" /></text><text x="32.400000000000006" y="46" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3545;0.3545;1;1" dur="11s" repeatCount="indefinite" /></text><text x="43.2" y="46" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3636;0.3636;1;1" dur="11s" repeatCount="indefinite" /></text><text x="54" y="46" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3727;0.3727;1;1" dur="11s" repeatCount="indefinite" /></text><text x="64.80000000000001" y="46" opacity="0">o<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3818;0.3818;1;1" dur="11s" repeatCount="indefinite" /></text><text y="69" opacity="0">name:&#160;positional&#160;argument&#160;missing<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3909;0.3909;1;1" dur="11s" repeatCount="indefinite" /></text><text y="92" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3909;0.3909;1;1" dur="11s" repeatCount="indefinite" /></text><text y="115" opacity="0">usage:<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3909;0.3909;1;1" dur="11s" repeatCount="indefinite" /></text><text y="138" opacity="0">&#160;&#160;hello&#160;name&#160;[--greeting]<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3909;0.3909;1;1" dur="11s" repeatCount="indefinite" /></text><text y="161" opacity="0" style="user-select: none;">$ <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3909;0.3909;1;1" dur="11s" repeatCount="indefinite" /></text><text x="21.6" y="161" opacity="0">h<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5818;0.5818;1;1" dur="11s" repeatCount="indefinite" /></text><text x="32.400000000000006" y="161" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5909;0.5909;1;1" dur="11s" repeatCount="indefinite" /></text><text x="43.2" y="161" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6;0.6;1;1" dur="11s" repeatCount="indefinite" /></text><text x="54" y="161" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6091;0.6091;1;1" dur="11s" repeatCount="indefinite" /></text><text x="64.80000000000001" y="161" opacity="0">o<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6182;0.6182;1;1" dur="11s" repeatCount="indefinite" /></text><text x="75.60000000000001" y="161" opacity="0"> <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6273;0.6273;1;1" dur="11s" repeatCount="indefinite" /></text><text x="86.4" y="161" opacity="0">w<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6364;0.6364;1;1" dur="11s" repeatCount="indefinite" /></text><text x="97.2" y="161" opacity="0">o<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6455;0.6455;1;1" dur="11s" repeatCount="indefinite" /></text><text x="108" y="161" opacity="0">r<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6545;0.6545;1;1" dur="11s" repeatCount="indefinite" /></text><text x="118.80000000000001" y="161" opacity="0">l<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6636;0.6636;1;1" dur="11s" repeatCount="indefinite" /></text><text x="129.60000000000002" y="161" opacity="0">d<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6727;0.6727;1;1" dur="11s" repeatCount="indefinite" /></text><text x="140.4" y="161" opacity="0"> <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6818;0.6818;1;1" dur="11s" repeatCount="indefinite" /></text><text x="151.20000000000002" y="161" opacity="0">-<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6909;0.6909;1;1" dur="11s" repeatCount="indefinite" /></text><text x="162" y="161" opacity="0">-<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7;0.7;1;1" dur="11s" repeatCount="indefinite" /></text><text x="172.8" y="161" opacity="0">g<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7091;0.7091;1;1" dur="11s" repeatCount="indefinite" /></text><text x="183.60000000000002" y="161" opacity="0">r<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7182;0.7182;1;1" dur="11s" repeatCount="indefinite" /></text><text x="194.4" y="161" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7273;0.7273;1;1" dur="11s" repeatCount="indefinite" /></text><text x="205.20000000000002" y="161" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7364;0.7364;1;1" dur="11s" repeatCount="indefinite" /></text><text x="216" y="161" opacity="0">t<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7455;0.7455;1;1" dur="11s" repeatCount="indefinite" /></text><text x="226.8" y="161" opacity="0">i<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7545;0.7545;1;1" dur="11s" repeatCount="indefinite" /></text><text x="237.60000000000002" y="161" opacity="0">n<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7636;0.7636;1;1" dur="11s" repeatCount="indefinite" /></text><text x="248.4" y="161" opacity="0">g<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7727;0.7727;1;1" dur="11s" repeatCount="indefinite" /></text><text x="259.20000000000005" y="161" opacity="0"> <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7818;0.7818;1;1" dur="11s" repeatCount="indefinite" /></text><text x="270" y="161" opacity="0">b<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.7909;0.7909;1;1" dur="11s" repeatCount="indefinite" /></text><text x="280.8" y="161" opacity="0">y<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.8;0.8;1;1" dur="11s" repeatCount="indefinite" /></text><text x="291.6" y="161" opacity="0">e<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.8091;0.8091;1;1" dur="11s" repeatCount="indefinite" /></text><text y="184" opacity="0">bye&#160;world<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.8182;0.8182;1;1" dur="11s" repeatCount="indefinite" /></text></g></svg>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magicli
3
- Version: 2.1.2
3
+ Version: 2.1.4
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,70 +13,64 @@ Dynamic: license-file
13
13
 
14
14
  # magiᴄʟɪ✨
15
15
 
16
- Automatically generates a CLI from functions of a module.
16
+ Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
17
17
 
18
- ## Install
18
+ **`hello world` example**
19
19
 
20
- ```
21
- pip install magicli
20
+ ```python
21
+ # hello.py
22
+ def hello(name, greeting="hello"):
23
+ print(greeting, name)
22
24
  ```
23
25
 
24
- ## Get started
26
+ becomes
25
27
 
26
- Simple usage example.
28
+ <img src="docs/img/hello.svg"/>
27
29
 
28
- ```python
29
- # module.py
30
- def hello(name, times=1):
31
- for _ in range(times):
32
- print("hello", name)
33
- ```
30
+ with only 3 commands
34
31
 
35
- Then run this in your terminal to initialize the CLI:
32
+ 1. `pip install magicli`
33
+ 1. `magicli`
34
+ 1. `pip install .`
36
35
 
37
- ```bash
38
- magicli
39
- ```
36
+ ## Quick start
40
37
 
41
- You are now able to install your package:
38
+ ### Install magicli
42
39
 
43
- ```bash
44
- pip install .
40
+ ```
41
+ pip install magicli
45
42
  ```
46
43
 
47
- And then call it:
44
+ ### Setup your repository
45
+
46
+ Initialize repo
48
47
 
49
48
  ```bash
50
- $ hello world --times 2
51
- hello world
52
- hello world
49
+ git init
53
50
  ```
54
51
 
55
- ### Define name of CLI in `pyproject.toml`
52
+ Add version info (optional)
56
53
 
57
- The terminal command `magicli` adds the following to your `pyproject.toml`.
58
-
59
- ```toml
60
- [project.scripts]
61
- hello = "magicli:magicli"
54
+ ```bash
55
+ git tag 1.0.0
62
56
  ```
63
57
 
64
- Important: Make sure the name of your CLI, the module name and the name of the function to be called are the same!
58
+ ### Automatically create CLI
65
59
 
66
60
  ```bash
67
- .
68
- ├── hello.py
69
- └── pyproject.toml
61
+ magicli
70
62
  ```
71
63
 
72
- You can now `pip install` your code and call it like this:
64
+ _Make sure the name of your CLI, the module name and the name of the function have to same name._
65
+
66
+ ### Install your Python package
73
67
 
74
68
  ```bash
75
- $ hello world --times 2
76
- hello world
77
- hello world
69
+ pip install .
78
70
  ```
79
71
 
72
+ ## Advanced use
73
+
80
74
  ### Using subcommands
81
75
 
82
76
  ```python
@@ -6,6 +6,7 @@ pyproject.toml
6
6
  .github/workflows/lint.yml
7
7
  .github/workflows/pytest.yml
8
8
  .github/workflows/release.yml
9
+ docs/img/hello.svg
9
10
  magicli.egg-info/PKG-INFO
10
11
  magicli.egg-info/SOURCES.txt
11
12
  magicli.egg-info/dependency_links.txt
@@ -15,6 +15,7 @@ from importlib import metadata
15
15
  from pathlib import Path
16
16
 
17
17
  logging.basicConfig(level=os.getenv("MAGICLI_LOG_LEVEL", "DEBUG"), format="%(message)s")
18
+ logger = logging.getLogger(__name__)
18
19
 
19
20
 
20
21
  class ParseArgvError(Exception):
@@ -73,8 +74,10 @@ def call(function, argv, module=None, name=None):
73
74
 
74
75
  try:
75
76
  args, kwargs = parse_argv(argv, parameters, docstring)
76
- except ParseArgvError:
77
- raise SystemExit(help_message(help_from_function, function, name))
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
81
 
79
82
  function(*args, **kwargs)
80
83
 
@@ -92,8 +95,8 @@ def parse_argv(argv, parameters, docstring):
92
95
  parse_short_options(key[1:], docstring, iter_argv, parameters, kwargs)
93
96
  else:
94
97
  if (index := len(args)) >= len(parameter_list):
95
- raise ParseArgvError
96
- args.append(get_type(parameter_list[index])(key))
98
+ raise ParseArgvError(f"{key}: unknown command")
99
+ args.append(cast_value(key, get_type(parameter_list[index])))
97
100
 
98
101
  check_all_args_present(len(args), parameter_list)
99
102
 
@@ -108,7 +111,9 @@ def check_all_args_present(len_args, parameter_list):
108
111
  if len_args < len(parameter_list):
109
112
  parameter = parameter_list[len_args]
110
113
  if parameter.default is parameter.empty:
111
- raise ParseArgvError
114
+ raise ParseArgvError(
115
+ f"{parameter_list[len_args].name}: positional argument missing"
116
+ )
112
117
 
113
118
 
114
119
  def parse_kwarg(key, argv, parameters):
@@ -119,16 +124,36 @@ def parse_kwarg(key, argv, parameters):
119
124
  """
120
125
  key, value = key.split("=", 1) if "=" in key else (key, None)
121
126
  key = key.replace("-", "_")
122
- cast_to = get_type(parameters.get(key))
127
+
128
+ if key not in parameters:
129
+ raise ParseArgvError(f"--{key}: unknown long option")
130
+
131
+ cast_to = get_type(parameters[key])
123
132
 
124
133
  if value is None:
125
134
  if cast_to is bool:
126
135
  return key, not parameters[key].default
127
136
  if cast_to is type(None):
128
137
  return key, True
129
- value = next(argv)
138
+ value = next_arg(argv)
139
+
140
+ return key, cast_value(value, cast_to)
141
+
130
142
 
131
- return key, value if cast_to is str else cast_to(value)
143
+ def next_arg(argv):
144
+ """Return the next command-line argument or raise a parser error."""
145
+ try:
146
+ return next(argv)
147
+ except StopIteration:
148
+ raise ParseArgvError("error: missing option value")
149
+
150
+
151
+ def cast_value(value, cast_to):
152
+ """Cast a command-line argument value or raise a parser error."""
153
+ try:
154
+ 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
132
157
 
133
158
 
134
159
  def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs):
@@ -137,7 +162,7 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
137
162
  long = short_to_long_option(short, docstring)
138
163
 
139
164
  if long not in parameters:
140
- raise SystemExit(f"--{long}: invalid long option")
165
+ raise ParseArgvError(f"--{long}: invalid long option")
141
166
 
142
167
  cast_to = get_type(parameters[long])
143
168
 
@@ -146,9 +171,9 @@ def parse_short_options(short_options, docstring, iter_argv, parameters, kwargs)
146
171
  elif cast_to is type(None):
147
172
  kwargs[long] = True
148
173
  elif i == len(short_options) - 1:
149
- kwargs[long] = cast_to(next(iter_argv))
174
+ kwargs[long] = cast_value(next_arg(iter_argv), cast_to)
150
175
  else:
151
- raise SystemExit(f"-{short}: invalid type")
176
+ raise ParseArgvError(f"-{short}: invalid type")
152
177
 
153
178
 
154
179
  def short_to_long_option(short, docstring):
@@ -160,7 +185,7 @@ def short_to_long_option(short, docstring):
160
185
  chars = [" ", "\n", "]"]
161
186
  indices = (i for char in chars if (i := docstring.find(char, start)) != -1)
162
187
  return docstring[start : min(indices, default=None)]
163
- raise SystemExit(f"-{short}: invalid short option")
188
+ raise ParseArgvError(f"-{short}: invalid short option")
164
189
 
165
190
 
166
191
  def get_type(parameter):
@@ -185,7 +210,7 @@ def check_for_version(argv, parameters, docstring, module):
185
210
  "-V": "-V, --version",
186
211
  }
187
212
  if (doc := args.get(argv[0])) and doc in docstring:
188
- logging.info(get_version(module))
213
+ logger.info(get_version(module))
189
214
  raise SystemExit
190
215
 
191
216
 
@@ -363,9 +388,9 @@ def cli(name="", author="", email="", description="", homepage=""):
363
388
  author = author or get_output("git config --get user.name")
364
389
  email = email or get_output("git config --get user.email")
365
390
  if not get_output("git tag"):
366
- logging.debug("Specify the version with `git tag`")
391
+ logger.debug("Specify the version with `git tag`")
367
392
  else:
368
- logging.debug("Not a git repo. Run `git init`")
393
+ logger.debug("Not a git repo. Run `git init`")
369
394
 
370
395
  authors = [f'{k}="{v}"' for k, v in {"name": author, "email": email}.items() if v]
371
396
 
@@ -387,7 +412,7 @@ def cli(name="", author="", email="", description="", homepage=""):
387
412
  if license_expression := get_license_expression(license_content):
388
413
  project.append(f'license = "{license_expression}"')
389
414
  else:
390
- logging.debug("Unknown license: %s", license_file.name)
415
+ logger.debug("Unknown license: %s", license_file.name)
391
416
  project.append(f'license-files = ["{license_file.name}"]')
392
417
 
393
418
  if description or (description := get_description(name)):
@@ -407,4 +432,4 @@ def cli(name="", author="", email="", description="", homepage=""):
407
432
  )
408
433
 
409
434
  pyproject.write_text(format_blocks(blocks, sep="\n") + "\n", encoding="utf-8")
410
- logging.debug("Created pyproject.toml ✨")
435
+ logger.debug("Created pyproject.toml ✨")
@@ -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"
magicli-2.1.2/README.md DELETED
@@ -1,96 +0,0 @@
1
- # magiᴄʟɪ✨
2
-
3
- Automatically generates a CLI from functions of a module.
4
-
5
- ## Install
6
-
7
- ```
8
- pip install magicli
9
- ```
10
-
11
- ## Get started
12
-
13
- Simple usage example.
14
-
15
- ```python
16
- # module.py
17
- def hello(name, times=1):
18
- for _ in range(times):
19
- print("hello", name)
20
- ```
21
-
22
- Then run this in your terminal to initialize the CLI:
23
-
24
- ```bash
25
- magicli
26
- ```
27
-
28
- You are now able to install your package:
29
-
30
- ```bash
31
- pip install .
32
- ```
33
-
34
- And then call it:
35
-
36
- ```bash
37
- $ hello world --times 2
38
- hello world
39
- hello world
40
- ```
41
-
42
- ### Define name of CLI in `pyproject.toml`
43
-
44
- The terminal command `magicli` adds the following to your `pyproject.toml`.
45
-
46
- ```toml
47
- [project.scripts]
48
- hello = "magicli:magicli"
49
- ```
50
-
51
- Important: Make sure the name of your CLI, the module name and the name of the function to be called are the same!
52
-
53
- ```bash
54
- .
55
- ├── hello.py
56
- └── pyproject.toml
57
- ```
58
-
59
- You can now `pip install` your code and call it like this:
60
-
61
- ```bash
62
- $ hello world --times 2
63
- hello world
64
- hello world
65
- ```
66
-
67
- ### Using subcommands
68
-
69
- ```python
70
- # module.py
71
- def hello(): ...
72
-
73
- def world(times=1):
74
- for _ in range(times):
75
- print("hello world")
76
- ```
77
-
78
- ```bash
79
- $ hello world --times 2
80
- hello world
81
- hello world
82
- ```
83
-
84
- ### Help message
85
-
86
- By default, the docstring of the function will be displayed.
87
-
88
- If no docstring is specified, an error message will be printed.
89
-
90
- ## Development
91
-
92
- Run pytest with coverage report:
93
-
94
- ```bash
95
- python3 -m pytest -s --cov=magicli --cov-report=term-missing
96
- ```
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes