clibind 0.0.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.
- clibind-0.0.1/LICENSE +21 -0
- clibind-0.0.1/PKG-INFO +169 -0
- clibind-0.0.1/README.md +156 -0
- clibind-0.0.1/pyproject.toml +25 -0
- clibind-0.0.1/setup.cfg +4 -0
- clibind-0.0.1/src/clibind/__init__.py +8 -0
- clibind-0.0.1/src/clibind/core.py +205 -0
- clibind-0.0.1/src/clibind.egg-info/PKG-INFO +169 -0
- clibind-0.0.1/src/clibind.egg-info/SOURCES.txt +9 -0
- clibind-0.0.1/src/clibind.egg-info/dependency_links.txt +1 -0
- clibind-0.0.1/src/clibind.egg-info/top_level.txt +1 -0
clibind-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 r8763754383405076
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
clibind-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clibind
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Bridge an argument parser and python callables seamlessly.
|
|
5
|
+
Author: r8763754383405076
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# The "clibind" Package
|
|
15
|
+
|
|
16
|
+
## Introduction
|
|
17
|
+
|
|
18
|
+
The `clibind` package provides comprehensive utilities that link CLI arguments from an `argparse.ArgumentParser` directly to Python callables (functions or classes). The core operational steps include:
|
|
19
|
+
1. Automatically configuring an argument parser based on the parameter signatures of a callable.
|
|
20
|
+
2. Building a valid keyword arguments (`kwargs`) dictionary from parsed CLI inputs to dynamically execute the target callable.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Preparation
|
|
25
|
+
|
|
26
|
+
To ensure `clibind` correctly parses parameter definitions, each tracked argument in your callable must meet at least one of the following criteria:
|
|
27
|
+
- Be assigned a explicit default value (other than `None`).
|
|
28
|
+
- Have an explicit Python type annotation.
|
|
29
|
+
|
|
30
|
+
### Supported Types
|
|
31
|
+
Currently, only the following core primitives are supported:
|
|
32
|
+
- `int`
|
|
33
|
+
- `float`
|
|
34
|
+
- `bool`
|
|
35
|
+
- `str`
|
|
36
|
+
|
|
37
|
+
*(Note: `Optional[T]` or `T | None` definitions are perfectly supported for the primitive types above).*
|
|
38
|
+
|
|
39
|
+
### Basic Decorator Configuration
|
|
40
|
+
Wrap your target callable object using the `@setup_clibind()` decorator. `setup_clibind` accepts two optional parameters:
|
|
41
|
+
- **`help_messages`** *(dict[str, str])*: Map explicit argument names to terminal help string messages.
|
|
42
|
+
- **`ignore_patterns`** *(list[str])*: Regex patterns matching arguments that should be skipped by the parser generator. By default, `"self"`, `"cls"`, and any variable names ending with an underscore (`_`) are automatically filtered out.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Important Constraints
|
|
47
|
+
|
|
48
|
+
When designing your callables for `clibind`, keep the following intentional design constraints in mind:
|
|
49
|
+
|
|
50
|
+
1. **Parameter Name Collisions**: If you map multiple callables to a single `argparse.ArgumentParser` instance (as shown in the application pattern below), you must ensure that their parameter names do not collide. Registering overlapping argument names to the same parser instance will cause `argparse` to raise an `ArgumentError`. It is the user's responsibility to maintain unique parameter names across combined callables.
|
|
51
|
+
2. **Mandatory Boolean Arguments**: Any parameter annotated with the `bool` type that lacks an explicit default value will automatically default to `False` and configure the parser with `action='store_true'`. This design choice ensures seamless integration with standard CLI toggle mechanics. If a strictly required boolean choice is needed, handle it via alternative validation or explicit defaults.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Usage Workflow
|
|
56
|
+
|
|
57
|
+
- **`setup_clibind`**: Attaches internal tracking metadata and analyzes target function/method definitions. If a class object is decorated, its `__init__` constructor method is evaluated.
|
|
58
|
+
- **`callable_to_parser`**: Registers tracked arguments safely onto a provided `argparse.ArgumentParser` instance.
|
|
59
|
+
- **`parser_to_callable`**: Extracts the structured keyword arguments (`kwargs`) matching the component signatures from the parsed execution context.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Example
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import argparse
|
|
67
|
+
import typing
|
|
68
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
69
|
+
|
|
70
|
+
@setup_clibind(
|
|
71
|
+
help_messages={
|
|
72
|
+
"var_1": "help message for var_1",
|
|
73
|
+
"var_a": "help message for var_a",
|
|
74
|
+
"var_f": "help message for var_f"
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
class TargetClass:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
var_to_skip_1_: list,
|
|
81
|
+
var_to_skip_2_: list,
|
|
82
|
+
var_1: int,
|
|
83
|
+
var_2: float,
|
|
84
|
+
var_3: str,
|
|
85
|
+
var_a=1,
|
|
86
|
+
var_b=1.1,
|
|
87
|
+
var_c="",
|
|
88
|
+
var_d1: int | None = None,
|
|
89
|
+
var_d2: typing.Optional[int] = None,
|
|
90
|
+
var_e=True,
|
|
91
|
+
var_f=False,
|
|
92
|
+
var_to_skip_3_=""
|
|
93
|
+
):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Initialize parser instance
|
|
97
|
+
parser = argparse.ArgumentParser()
|
|
98
|
+
callable_to_parser(obj=TargetClass, parser=parser)
|
|
99
|
+
|
|
100
|
+
# View generated help documentation
|
|
101
|
+
parser.print_help()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Generated Layout
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
usage: main.py [-h] --var-1 INT --var-2 FLOAT --var-3 STR
|
|
108
|
+
[--var-a INT] [--var-b FLOAT] [--var-c STR]
|
|
109
|
+
[--var-d1 INT] [--var-d2 INT] [--var-e-toggle]
|
|
110
|
+
[--var-f-toggle]
|
|
111
|
+
|
|
112
|
+
options:
|
|
113
|
+
-h, --help show this help message and exit
|
|
114
|
+
|
|
115
|
+
arguments for "__main__.TargetClass":
|
|
116
|
+
--var-1 INT help message for var_1
|
|
117
|
+
--var-2 FLOAT
|
|
118
|
+
--var-3 STR
|
|
119
|
+
--var-a INT (default: 1) help message for var_a
|
|
120
|
+
--var-b FLOAT (default: 1.1)
|
|
121
|
+
--var-c STR (default: '')
|
|
122
|
+
--var-d1 INT (default: None)
|
|
123
|
+
--var-d2 INT (default: None)
|
|
124
|
+
--var-e-toggle (default: True)
|
|
125
|
+
--var-f-toggle (default: False) help message for var_f
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Production / Application Pattern
|
|
129
|
+
|
|
130
|
+
In extensive systems (such as machine learning training pipelines), this architecture cleans up modular execution flows:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# main.py
|
|
134
|
+
import argparse
|
|
135
|
+
import experiments.train
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
parser = argparse.ArgumentParser()
|
|
139
|
+
subparsers = parser.add_subparsers()
|
|
140
|
+
|
|
141
|
+
parser_train = subparsers.add_parser("train")
|
|
142
|
+
experiments.train.setup(parser_train)
|
|
143
|
+
parser_train.set_defaults(func=experiments.train.handle)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# experiments/train.py
|
|
147
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
148
|
+
|
|
149
|
+
def setup(parser):
|
|
150
|
+
callable_to_parser(read_data, parser)
|
|
151
|
+
callable_to_parser(run, parser)
|
|
152
|
+
|
|
153
|
+
def handle(args):
|
|
154
|
+
data_reader_args = parser_to_callable(args, read_data)
|
|
155
|
+
run_args = parser_to_callable(args, run)
|
|
156
|
+
|
|
157
|
+
# Safely forward mapped variables
|
|
158
|
+
run(data_reader_args_=data_reader_args, **run_args)
|
|
159
|
+
|
|
160
|
+
@setup_clibind()
|
|
161
|
+
def run(data_reader_args_: dict, epochs: int = 10):
|
|
162
|
+
train_ds, val_ds = read_data(**data_reader_args_)
|
|
163
|
+
# Train execution logic...
|
|
164
|
+
|
|
165
|
+
@setup_clibind()
|
|
166
|
+
def read_data(dataset_dir: str):
|
|
167
|
+
# Data extraction logic...
|
|
168
|
+
pass
|
|
169
|
+
```
|
clibind-0.0.1/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# The "clibind" Package
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
The `clibind` package provides comprehensive utilities that link CLI arguments from an `argparse.ArgumentParser` directly to Python callables (functions or classes). The core operational steps include:
|
|
6
|
+
1. Automatically configuring an argument parser based on the parameter signatures of a callable.
|
|
7
|
+
2. Building a valid keyword arguments (`kwargs`) dictionary from parsed CLI inputs to dynamically execute the target callable.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Preparation
|
|
12
|
+
|
|
13
|
+
To ensure `clibind` correctly parses parameter definitions, each tracked argument in your callable must meet at least one of the following criteria:
|
|
14
|
+
- Be assigned a explicit default value (other than `None`).
|
|
15
|
+
- Have an explicit Python type annotation.
|
|
16
|
+
|
|
17
|
+
### Supported Types
|
|
18
|
+
Currently, only the following core primitives are supported:
|
|
19
|
+
- `int`
|
|
20
|
+
- `float`
|
|
21
|
+
- `bool`
|
|
22
|
+
- `str`
|
|
23
|
+
|
|
24
|
+
*(Note: `Optional[T]` or `T | None` definitions are perfectly supported for the primitive types above).*
|
|
25
|
+
|
|
26
|
+
### Basic Decorator Configuration
|
|
27
|
+
Wrap your target callable object using the `@setup_clibind()` decorator. `setup_clibind` accepts two optional parameters:
|
|
28
|
+
- **`help_messages`** *(dict[str, str])*: Map explicit argument names to terminal help string messages.
|
|
29
|
+
- **`ignore_patterns`** *(list[str])*: Regex patterns matching arguments that should be skipped by the parser generator. By default, `"self"`, `"cls"`, and any variable names ending with an underscore (`_`) are automatically filtered out.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Important Constraints
|
|
34
|
+
|
|
35
|
+
When designing your callables for `clibind`, keep the following intentional design constraints in mind:
|
|
36
|
+
|
|
37
|
+
1. **Parameter Name Collisions**: If you map multiple callables to a single `argparse.ArgumentParser` instance (as shown in the application pattern below), you must ensure that their parameter names do not collide. Registering overlapping argument names to the same parser instance will cause `argparse` to raise an `ArgumentError`. It is the user's responsibility to maintain unique parameter names across combined callables.
|
|
38
|
+
2. **Mandatory Boolean Arguments**: Any parameter annotated with the `bool` type that lacks an explicit default value will automatically default to `False` and configure the parser with `action='store_true'`. This design choice ensures seamless integration with standard CLI toggle mechanics. If a strictly required boolean choice is needed, handle it via alternative validation or explicit defaults.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Usage Workflow
|
|
43
|
+
|
|
44
|
+
- **`setup_clibind`**: Attaches internal tracking metadata and analyzes target function/method definitions. If a class object is decorated, its `__init__` constructor method is evaluated.
|
|
45
|
+
- **`callable_to_parser`**: Registers tracked arguments safely onto a provided `argparse.ArgumentParser` instance.
|
|
46
|
+
- **`parser_to_callable`**: Extracts the structured keyword arguments (`kwargs`) matching the component signatures from the parsed execution context.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import argparse
|
|
54
|
+
import typing
|
|
55
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
56
|
+
|
|
57
|
+
@setup_clibind(
|
|
58
|
+
help_messages={
|
|
59
|
+
"var_1": "help message for var_1",
|
|
60
|
+
"var_a": "help message for var_a",
|
|
61
|
+
"var_f": "help message for var_f"
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
class TargetClass:
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
var_to_skip_1_: list,
|
|
68
|
+
var_to_skip_2_: list,
|
|
69
|
+
var_1: int,
|
|
70
|
+
var_2: float,
|
|
71
|
+
var_3: str,
|
|
72
|
+
var_a=1,
|
|
73
|
+
var_b=1.1,
|
|
74
|
+
var_c="",
|
|
75
|
+
var_d1: int | None = None,
|
|
76
|
+
var_d2: typing.Optional[int] = None,
|
|
77
|
+
var_e=True,
|
|
78
|
+
var_f=False,
|
|
79
|
+
var_to_skip_3_=""
|
|
80
|
+
):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Initialize parser instance
|
|
84
|
+
parser = argparse.ArgumentParser()
|
|
85
|
+
callable_to_parser(obj=TargetClass, parser=parser)
|
|
86
|
+
|
|
87
|
+
# View generated help documentation
|
|
88
|
+
parser.print_help()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Generated Layout
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
usage: main.py [-h] --var-1 INT --var-2 FLOAT --var-3 STR
|
|
95
|
+
[--var-a INT] [--var-b FLOAT] [--var-c STR]
|
|
96
|
+
[--var-d1 INT] [--var-d2 INT] [--var-e-toggle]
|
|
97
|
+
[--var-f-toggle]
|
|
98
|
+
|
|
99
|
+
options:
|
|
100
|
+
-h, --help show this help message and exit
|
|
101
|
+
|
|
102
|
+
arguments for "__main__.TargetClass":
|
|
103
|
+
--var-1 INT help message for var_1
|
|
104
|
+
--var-2 FLOAT
|
|
105
|
+
--var-3 STR
|
|
106
|
+
--var-a INT (default: 1) help message for var_a
|
|
107
|
+
--var-b FLOAT (default: 1.1)
|
|
108
|
+
--var-c STR (default: '')
|
|
109
|
+
--var-d1 INT (default: None)
|
|
110
|
+
--var-d2 INT (default: None)
|
|
111
|
+
--var-e-toggle (default: True)
|
|
112
|
+
--var-f-toggle (default: False) help message for var_f
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Production / Application Pattern
|
|
116
|
+
|
|
117
|
+
In extensive systems (such as machine learning training pipelines), this architecture cleans up modular execution flows:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# main.py
|
|
121
|
+
import argparse
|
|
122
|
+
import experiments.train
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
parser = argparse.ArgumentParser()
|
|
126
|
+
subparsers = parser.add_subparsers()
|
|
127
|
+
|
|
128
|
+
parser_train = subparsers.add_parser("train")
|
|
129
|
+
experiments.train.setup(parser_train)
|
|
130
|
+
parser_train.set_defaults(func=experiments.train.handle)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# experiments/train.py
|
|
134
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
135
|
+
|
|
136
|
+
def setup(parser):
|
|
137
|
+
callable_to_parser(read_data, parser)
|
|
138
|
+
callable_to_parser(run, parser)
|
|
139
|
+
|
|
140
|
+
def handle(args):
|
|
141
|
+
data_reader_args = parser_to_callable(args, read_data)
|
|
142
|
+
run_args = parser_to_callable(args, run)
|
|
143
|
+
|
|
144
|
+
# Safely forward mapped variables
|
|
145
|
+
run(data_reader_args_=data_reader_args, **run_args)
|
|
146
|
+
|
|
147
|
+
@setup_clibind()
|
|
148
|
+
def run(data_reader_args_: dict, epochs: int = 10):
|
|
149
|
+
train_ds, val_ds = read_data(**data_reader_args_)
|
|
150
|
+
# Train execution logic...
|
|
151
|
+
|
|
152
|
+
@setup_clibind()
|
|
153
|
+
def read_data(dataset_dir: str):
|
|
154
|
+
# Data extraction logic...
|
|
155
|
+
pass
|
|
156
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "clibind"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name="r8763754383405076" },
|
|
9
|
+
]
|
|
10
|
+
description = "Bridge an argument parser and python callables seamlessly."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = ["LICEN[CS]E*"]
|
|
19
|
+
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
dynamic = ["version"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.dynamic]
|
|
25
|
+
version = {attr = "clibind.__version__"}
|
clibind-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
clibind: Bridge an argument parser and python callables seamlessly.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core import setup_clibind, callable_to_parser, parser_to_callable
|
|
6
|
+
|
|
7
|
+
__version__: str = "0.0.1"
|
|
8
|
+
__all__: list[str] = ["setup_clibind", "callable_to_parser", "parser_to_callable"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The "clibind" Package
|
|
3
|
+
|
|
4
|
+
Bridge an argument parser and python callables seamlessly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import inspect
|
|
9
|
+
import re
|
|
10
|
+
import types
|
|
11
|
+
import typing
|
|
12
|
+
from typing import Any, Callable, TypeVar
|
|
13
|
+
|
|
14
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup_clibind(
|
|
18
|
+
help_messages: dict[str, str] | None = None,
|
|
19
|
+
ignore_patterns: list[str] | None = None
|
|
20
|
+
) -> Callable[[_F], _F]:
|
|
21
|
+
"""
|
|
22
|
+
A decorator to set up "clibind" metadata directly on a callable or class object.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
help_messages (dict[str, str] | None): Dictionary mapping argument names to their help strings.
|
|
26
|
+
ignore_patterns (list[str] | None): Regex patterns to exclude specific arguments from parser generation.
|
|
27
|
+
If None, defaults to ignoring "self", "cls", and any argument ending with an underscore "_".
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Callable[[_F], _F]: The decorator function that attaches configuration attributes directly to the target.
|
|
31
|
+
"""
|
|
32
|
+
def decorator(obj: _F) -> _F:
|
|
33
|
+
# Utilizing Python's class attribute shadowing:
|
|
34
|
+
# Writing directly to "obj" isolates metadata for inherited sub-classes automatically.
|
|
35
|
+
obj._clibind_help_messages = help_messages
|
|
36
|
+
if ignore_patterns is None:
|
|
37
|
+
obj._clibind_ignore_patterns = [r"^self$", r"^cls$", r"^.*_$"]
|
|
38
|
+
else:
|
|
39
|
+
obj._clibind_ignore_patterns = ignore_patterns
|
|
40
|
+
|
|
41
|
+
obj._clibind_callable_args_to_parser_args = {}
|
|
42
|
+
obj._clibind_callable_args_to_parser_configs = {}
|
|
43
|
+
|
|
44
|
+
_analyze_callable_args(obj)
|
|
45
|
+
return obj
|
|
46
|
+
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def callable_to_parser(
|
|
51
|
+
obj: Any,
|
|
52
|
+
parser: argparse.ArgumentParser
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Extracts analyzed metadata from a registered target and registers its arguments into the parser.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
obj (Any): A callable or class decorated with `@setup_clibind`.
|
|
59
|
+
parser (argparse.ArgumentParser): The target argument parser instance.
|
|
60
|
+
"""
|
|
61
|
+
group_title = f"arguments for \"{_get_obj_identifier(obj)}\""
|
|
62
|
+
group = parser.add_argument_group(group_title)
|
|
63
|
+
|
|
64
|
+
for callable_arg, parser_arg in obj._clibind_callable_args_to_parser_args.items():
|
|
65
|
+
parser_flag = _get_parser_flag(parser_arg)
|
|
66
|
+
parser_config = obj._clibind_callable_args_to_parser_configs[callable_arg]
|
|
67
|
+
group.add_argument(parser_flag, **parser_config)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parser_to_callable(
|
|
71
|
+
args: argparse.Namespace | dict[str, Any],
|
|
72
|
+
obj: Any
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Constructs a keyword arguments dictionary for the target from parsed arguments.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
args (argparse.Namespace | dict[str, Any]): The parsed arguments from `parser.parse_args()`.
|
|
79
|
+
obj (Any): The target callable or class decorated with `@setup_clibind`.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict[str, Any]: A dictionary of mapped arguments ready to be unpacked into the target.
|
|
83
|
+
"""
|
|
84
|
+
args_dict = vars(args) if not isinstance(args, dict) else args
|
|
85
|
+
|
|
86
|
+
callable_kwargs_dict: dict[str, Any] = {}
|
|
87
|
+
for callable_arg, parser_arg in obj._clibind_callable_args_to_parser_args.items():
|
|
88
|
+
callable_kwargs_dict[callable_arg] = args_dict[parser_arg]
|
|
89
|
+
|
|
90
|
+
return callable_kwargs_dict
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _analyze_callable_args(obj: Any) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Inspects the signature of the target object and populates its internal configuration.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
obj (Any): The target class or function to inspect.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
RuntimeError: If unsupported parameter kinds, invalid types, or invalid annotations are detected.
|
|
102
|
+
"""
|
|
103
|
+
# inspect.signature automatically extracts __init__ signature without "self" when a class is passed.
|
|
104
|
+
# eval_str=True dynamically resolves string annotations (from __future__ import annotations)
|
|
105
|
+
sig = inspect.signature(obj, eval_str=True)
|
|
106
|
+
|
|
107
|
+
for param in sig.parameters.values():
|
|
108
|
+
if any(re.fullmatch(pattern, param.name) for pattern in obj._clibind_ignore_patterns):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if param.kind != param.POSITIONAL_OR_KEYWORD:
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
f"Parameter '{param.name}' in {_get_obj_identifier(obj)} is invalid. "
|
|
114
|
+
"Only POSITIONAL_OR_KEYWORD parameters are supported."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
param_has_annotation = param.annotation is not param.empty
|
|
118
|
+
param_has_default = param.default is not param.empty
|
|
119
|
+
|
|
120
|
+
if param_has_annotation:
|
|
121
|
+
if isinstance(param.annotation, str):
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"Parameter '{param.name}' in {_get_obj_identifier(obj)} "
|
|
124
|
+
"uses an unresolvable string forward-reference annotation."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
annotation_origin = typing.get_origin(param.annotation)
|
|
128
|
+
if annotation_origin is None:
|
|
129
|
+
param_type = param.annotation
|
|
130
|
+
else:
|
|
131
|
+
# Handle Union types (e.g., Optional[T] or T | None)
|
|
132
|
+
annotation_acceptable = False
|
|
133
|
+
if annotation_origin in (typing.Union, types.UnionType):
|
|
134
|
+
annotation_args = typing.get_args(param.annotation)
|
|
135
|
+
non_none = [t for t in annotation_args if t is not type(None)]
|
|
136
|
+
if len(non_none) == 1:
|
|
137
|
+
param_type = non_none[0]
|
|
138
|
+
annotation_acceptable = True
|
|
139
|
+
|
|
140
|
+
if not annotation_acceptable:
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
f"Annotation of '{param.name}' in {_get_obj_identifier(obj)} "
|
|
143
|
+
f"({param.annotation}) is not supported."
|
|
144
|
+
)
|
|
145
|
+
elif param_has_default:
|
|
146
|
+
param_type = type(param.default)
|
|
147
|
+
else:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"Parameter '{param.name}' must have either a clear type annotation or a default value."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if param_type not in (int, float, bool, str):
|
|
153
|
+
raise RuntimeError(
|
|
154
|
+
"Only int, float, bool, and str types are supported, but "
|
|
155
|
+
f"'{param.name}' in {_get_obj_identifier(obj)} is evaluated as {param_type}."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
param_default_value = param.default if param_has_default else None
|
|
159
|
+
|
|
160
|
+
if param_type is bool and param_default_value is None:
|
|
161
|
+
param_default_value = False
|
|
162
|
+
|
|
163
|
+
parser_arg = param.name
|
|
164
|
+
if param_type is bool:
|
|
165
|
+
parser_arg += "_toggle"
|
|
166
|
+
obj._clibind_callable_args_to_parser_args[param.name] = parser_arg
|
|
167
|
+
|
|
168
|
+
param_help = obj._clibind_help_messages.get(param.name, "") if obj._clibind_help_messages else ""
|
|
169
|
+
|
|
170
|
+
if param_type is bool:
|
|
171
|
+
parser_config: dict[str, Any] = {
|
|
172
|
+
"action": "store_false" if param_default_value else "store_true",
|
|
173
|
+
"help": f"(default: {repr(param_default_value)}) {param_help}".strip()
|
|
174
|
+
}
|
|
175
|
+
else:
|
|
176
|
+
if param_has_default:
|
|
177
|
+
parser_config = {
|
|
178
|
+
"type": param_type,
|
|
179
|
+
"default": param_default_value,
|
|
180
|
+
"metavar": param_type.__name__.upper(),
|
|
181
|
+
"help": f"(default: {repr(param_default_value)}) {param_help}".strip()
|
|
182
|
+
}
|
|
183
|
+
else:
|
|
184
|
+
parser_config = {
|
|
185
|
+
"type": param_type,
|
|
186
|
+
"required": True,
|
|
187
|
+
"metavar": param_type.__name__.upper(),
|
|
188
|
+
"help": param_help.strip()
|
|
189
|
+
}
|
|
190
|
+
obj._clibind_callable_args_to_parser_configs[param.name] = parser_config
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _get_obj_identifier(obj: Any) -> str:
|
|
194
|
+
"""Generates a fully qualified identifier string for a class or function."""
|
|
195
|
+
return f"{obj.__module__}.{obj.__qualname__}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _get_parser_flag(parser_arg: str) -> str:
|
|
199
|
+
"""Transforms a snake_case argument name into a standard CLI double-dash flag style."""
|
|
200
|
+
return "--" + parser_arg.replace("_", "-")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
import doctest
|
|
205
|
+
doctest.testmod(verbose=True)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clibind
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Bridge an argument parser and python callables seamlessly.
|
|
5
|
+
Author: r8763754383405076
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# The "clibind" Package
|
|
15
|
+
|
|
16
|
+
## Introduction
|
|
17
|
+
|
|
18
|
+
The `clibind` package provides comprehensive utilities that link CLI arguments from an `argparse.ArgumentParser` directly to Python callables (functions or classes). The core operational steps include:
|
|
19
|
+
1. Automatically configuring an argument parser based on the parameter signatures of a callable.
|
|
20
|
+
2. Building a valid keyword arguments (`kwargs`) dictionary from parsed CLI inputs to dynamically execute the target callable.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Preparation
|
|
25
|
+
|
|
26
|
+
To ensure `clibind` correctly parses parameter definitions, each tracked argument in your callable must meet at least one of the following criteria:
|
|
27
|
+
- Be assigned a explicit default value (other than `None`).
|
|
28
|
+
- Have an explicit Python type annotation.
|
|
29
|
+
|
|
30
|
+
### Supported Types
|
|
31
|
+
Currently, only the following core primitives are supported:
|
|
32
|
+
- `int`
|
|
33
|
+
- `float`
|
|
34
|
+
- `bool`
|
|
35
|
+
- `str`
|
|
36
|
+
|
|
37
|
+
*(Note: `Optional[T]` or `T | None` definitions are perfectly supported for the primitive types above).*
|
|
38
|
+
|
|
39
|
+
### Basic Decorator Configuration
|
|
40
|
+
Wrap your target callable object using the `@setup_clibind()` decorator. `setup_clibind` accepts two optional parameters:
|
|
41
|
+
- **`help_messages`** *(dict[str, str])*: Map explicit argument names to terminal help string messages.
|
|
42
|
+
- **`ignore_patterns`** *(list[str])*: Regex patterns matching arguments that should be skipped by the parser generator. By default, `"self"`, `"cls"`, and any variable names ending with an underscore (`_`) are automatically filtered out.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Important Constraints
|
|
47
|
+
|
|
48
|
+
When designing your callables for `clibind`, keep the following intentional design constraints in mind:
|
|
49
|
+
|
|
50
|
+
1. **Parameter Name Collisions**: If you map multiple callables to a single `argparse.ArgumentParser` instance (as shown in the application pattern below), you must ensure that their parameter names do not collide. Registering overlapping argument names to the same parser instance will cause `argparse` to raise an `ArgumentError`. It is the user's responsibility to maintain unique parameter names across combined callables.
|
|
51
|
+
2. **Mandatory Boolean Arguments**: Any parameter annotated with the `bool` type that lacks an explicit default value will automatically default to `False` and configure the parser with `action='store_true'`. This design choice ensures seamless integration with standard CLI toggle mechanics. If a strictly required boolean choice is needed, handle it via alternative validation or explicit defaults.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Usage Workflow
|
|
56
|
+
|
|
57
|
+
- **`setup_clibind`**: Attaches internal tracking metadata and analyzes target function/method definitions. If a class object is decorated, its `__init__` constructor method is evaluated.
|
|
58
|
+
- **`callable_to_parser`**: Registers tracked arguments safely onto a provided `argparse.ArgumentParser` instance.
|
|
59
|
+
- **`parser_to_callable`**: Extracts the structured keyword arguments (`kwargs`) matching the component signatures from the parsed execution context.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Example
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import argparse
|
|
67
|
+
import typing
|
|
68
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
69
|
+
|
|
70
|
+
@setup_clibind(
|
|
71
|
+
help_messages={
|
|
72
|
+
"var_1": "help message for var_1",
|
|
73
|
+
"var_a": "help message for var_a",
|
|
74
|
+
"var_f": "help message for var_f"
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
class TargetClass:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
var_to_skip_1_: list,
|
|
81
|
+
var_to_skip_2_: list,
|
|
82
|
+
var_1: int,
|
|
83
|
+
var_2: float,
|
|
84
|
+
var_3: str,
|
|
85
|
+
var_a=1,
|
|
86
|
+
var_b=1.1,
|
|
87
|
+
var_c="",
|
|
88
|
+
var_d1: int | None = None,
|
|
89
|
+
var_d2: typing.Optional[int] = None,
|
|
90
|
+
var_e=True,
|
|
91
|
+
var_f=False,
|
|
92
|
+
var_to_skip_3_=""
|
|
93
|
+
):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Initialize parser instance
|
|
97
|
+
parser = argparse.ArgumentParser()
|
|
98
|
+
callable_to_parser(obj=TargetClass, parser=parser)
|
|
99
|
+
|
|
100
|
+
# View generated help documentation
|
|
101
|
+
parser.print_help()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Generated Layout
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
usage: main.py [-h] --var-1 INT --var-2 FLOAT --var-3 STR
|
|
108
|
+
[--var-a INT] [--var-b FLOAT] [--var-c STR]
|
|
109
|
+
[--var-d1 INT] [--var-d2 INT] [--var-e-toggle]
|
|
110
|
+
[--var-f-toggle]
|
|
111
|
+
|
|
112
|
+
options:
|
|
113
|
+
-h, --help show this help message and exit
|
|
114
|
+
|
|
115
|
+
arguments for "__main__.TargetClass":
|
|
116
|
+
--var-1 INT help message for var_1
|
|
117
|
+
--var-2 FLOAT
|
|
118
|
+
--var-3 STR
|
|
119
|
+
--var-a INT (default: 1) help message for var_a
|
|
120
|
+
--var-b FLOAT (default: 1.1)
|
|
121
|
+
--var-c STR (default: '')
|
|
122
|
+
--var-d1 INT (default: None)
|
|
123
|
+
--var-d2 INT (default: None)
|
|
124
|
+
--var-e-toggle (default: True)
|
|
125
|
+
--var-f-toggle (default: False) help message for var_f
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Production / Application Pattern
|
|
129
|
+
|
|
130
|
+
In extensive systems (such as machine learning training pipelines), this architecture cleans up modular execution flows:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# main.py
|
|
134
|
+
import argparse
|
|
135
|
+
import experiments.train
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
parser = argparse.ArgumentParser()
|
|
139
|
+
subparsers = parser.add_subparsers()
|
|
140
|
+
|
|
141
|
+
parser_train = subparsers.add_parser("train")
|
|
142
|
+
experiments.train.setup(parser_train)
|
|
143
|
+
parser_train.set_defaults(func=experiments.train.handle)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# experiments/train.py
|
|
147
|
+
from clibind import setup_clibind, callable_to_parser, parser_to_callable
|
|
148
|
+
|
|
149
|
+
def setup(parser):
|
|
150
|
+
callable_to_parser(read_data, parser)
|
|
151
|
+
callable_to_parser(run, parser)
|
|
152
|
+
|
|
153
|
+
def handle(args):
|
|
154
|
+
data_reader_args = parser_to_callable(args, read_data)
|
|
155
|
+
run_args = parser_to_callable(args, run)
|
|
156
|
+
|
|
157
|
+
# Safely forward mapped variables
|
|
158
|
+
run(data_reader_args_=data_reader_args, **run_args)
|
|
159
|
+
|
|
160
|
+
@setup_clibind()
|
|
161
|
+
def run(data_reader_args_: dict, epochs: int = 10):
|
|
162
|
+
train_ds, val_ds = read_data(**data_reader_args_)
|
|
163
|
+
# Train execution logic...
|
|
164
|
+
|
|
165
|
+
@setup_clibind()
|
|
166
|
+
def read_data(dataset_dir: str):
|
|
167
|
+
# Data extraction logic...
|
|
168
|
+
pass
|
|
169
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clibind
|