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.
- {magicli-2.1.2 → magicli-2.1.4}/.github/workflows/lint.yml +9 -8
- {magicli-2.1.2 → magicli-2.1.4}/.github/workflows/pytest.yml +3 -2
- {magicli-2.1.2 → magicli-2.1.4}/.github/workflows/release.yml +2 -2
- {magicli-2.1.2 → magicli-2.1.4}/PKG-INFO +32 -38
- magicli-2.1.4/README.md +90 -0
- magicli-2.1.4/docs/img/hello.svg +1 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/PKG-INFO +32 -38
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/SOURCES.txt +1 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.py +42 -17
- {magicli-2.1.2 → magicli-2.1.4}/tests/test_magicli.py +5 -3
- {magicli-2.1.2 → magicli-2.1.4}/tests/test_parse_kwarg.py +35 -3
- {magicli-2.1.2 → magicli-2.1.4}/tests/test_parse_short_options.py +11 -10
- magicli-2.1.2/README.md +0 -96
- {magicli-2.1.2 → magicli-2.1.4}/.gitignore +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/LICENSE +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/dependency_links.txt +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/entry_points.txt +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/requires.txt +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/magicli.egg-info/top_level.txt +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/pyproject.toml +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/setup.cfg +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/tests/fixtures.py +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/tests/test_cli.py +0 -0
- {magicli-2.1.2 → magicli-2.1.4}/tests/test_help.py +0 -0
|
@@ -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@
|
|
13
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
31
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
40
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
17
|
-
- uses: astral-sh/setup-uv@
|
|
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 }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magicli
|
|
3
|
-
Version: 2.1.
|
|
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
|
|
16
|
+
Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**`hello world` example**
|
|
19
19
|
|
|
20
|
-
```
|
|
21
|
-
|
|
20
|
+
```python
|
|
21
|
+
# hello.py
|
|
22
|
+
def hello(name, greeting="hello"):
|
|
23
|
+
print(greeting, name)
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
becomes
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
<img src="docs/img/hello.svg"/>
|
|
27
29
|
|
|
28
|
-
|
|
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
|
-
|
|
32
|
+
1. `pip install magicli`
|
|
33
|
+
1. `magicli`
|
|
34
|
+
1. `pip install .`
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
magicli
|
|
39
|
-
```
|
|
36
|
+
## Quick start
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
### Install magicli
|
|
42
39
|
|
|
43
|
-
```
|
|
44
|
-
pip install
|
|
40
|
+
```
|
|
41
|
+
pip install magicli
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
### Setup your repository
|
|
45
|
+
|
|
46
|
+
Initialize repo
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
|
-
|
|
51
|
-
hello world
|
|
52
|
-
hello world
|
|
49
|
+
git init
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
Add version info (optional)
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```toml
|
|
60
|
-
[project.scripts]
|
|
61
|
-
hello = "magicli:magicli"
|
|
54
|
+
```bash
|
|
55
|
+
git tag 1.0.0
|
|
62
56
|
```
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
### Automatically create CLI
|
|
65
59
|
|
|
66
60
|
```bash
|
|
67
|
-
|
|
68
|
-
├── hello.py
|
|
69
|
-
└── pyproject.toml
|
|
61
|
+
magicli
|
|
70
62
|
```
|
|
71
63
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
magicli-2.1.4/README.md
ADDED
|
@@ -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 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: positional argument 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">  hello name [--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 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.
|
|
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
|
|
16
|
+
Automatically turns a file into a CLI without any boilerplate by introspecting its functions.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**`hello world` example**
|
|
19
19
|
|
|
20
|
-
```
|
|
21
|
-
|
|
20
|
+
```python
|
|
21
|
+
# hello.py
|
|
22
|
+
def hello(name, greeting="hello"):
|
|
23
|
+
print(greeting, name)
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
becomes
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
<img src="docs/img/hello.svg"/>
|
|
27
29
|
|
|
28
|
-
|
|
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
|
-
|
|
32
|
+
1. `pip install magicli`
|
|
33
|
+
1. `magicli`
|
|
34
|
+
1. `pip install .`
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
magicli
|
|
39
|
-
```
|
|
36
|
+
## Quick start
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
### Install magicli
|
|
42
39
|
|
|
43
|
-
```
|
|
44
|
-
pip install
|
|
40
|
+
```
|
|
41
|
+
pip install magicli
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
### Setup your repository
|
|
45
|
+
|
|
46
|
+
Initialize repo
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
|
-
|
|
51
|
-
hello world
|
|
52
|
-
hello world
|
|
49
|
+
git init
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
Add version info (optional)
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```toml
|
|
60
|
-
[project.scripts]
|
|
61
|
-
hello = "magicli:magicli"
|
|
54
|
+
```bash
|
|
55
|
+
git tag 1.0.0
|
|
62
56
|
```
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
### Automatically create CLI
|
|
65
59
|
|
|
66
60
|
```bash
|
|
67
|
-
|
|
68
|
-
├── hello.py
|
|
69
|
-
└── pyproject.toml
|
|
61
|
+
magicli
|
|
70
62
|
```
|
|
71
63
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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])
|
|
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
|
-
|
|
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 =
|
|
138
|
+
value = next_arg(argv)
|
|
139
|
+
|
|
140
|
+
return key, cast_value(value, cast_to)
|
|
141
|
+
|
|
130
142
|
|
|
131
|
-
|
|
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
|
|
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] =
|
|
174
|
+
kwargs[long] = cast_value(next_arg(iter_argv), cast_to)
|
|
150
175
|
else:
|
|
151
|
-
raise
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
391
|
+
logger.debug("Specify the version with `git tag`")
|
|
367
392
|
else:
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
45
|
-
parse_short_options(**(
|
|
46
|
-
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|