simpleArgParser 0.1.0__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.
- simpleargparser-0.1.0/PKG-INFO +35 -0
- simpleargparser-0.1.0/README.md +15 -0
- simpleargparser-0.1.0/setup.cfg +4 -0
- simpleargparser-0.1.0/setup.py +22 -0
- simpleargparser-0.1.0/simpleArgParser/__init__.py +9 -0
- simpleargparser-0.1.0/simpleArgParser/s_argparse.py +302 -0
- simpleargparser-0.1.0/simpleArgParser.egg-info/PKG-INFO +35 -0
- simpleargparser-0.1.0/simpleArgParser.egg-info/SOURCES.txt +8 -0
- simpleargparser-0.1.0/simpleArgParser.egg-info/dependency_links.txt +1 -0
- simpleargparser-0.1.0/simpleArgParser.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: simpleArgParser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple typed argument parser using dataclasses and type hints. This project is largely generated by ChatGPT.
|
|
5
|
+
Home-page: https://github.com/Raibows/SimpleArgParser
|
|
6
|
+
Author: raibows
|
|
7
|
+
Author-email: raibows@hotmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# SimpleArgParser
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
This is a simple command line argument parser encapsulated based on Python dataclasses and type hints, supporting:
|
|
28
|
+
- Defining arguments using classes (required, optional, and arguments with default values)
|
|
29
|
+
- Nested dataclasses, with argument names separated by dots
|
|
30
|
+
- JSON configuration file loading (priority: command line > code input > JSON config > default value)
|
|
31
|
+
- List type arguments (supports comma separation)
|
|
32
|
+
- Enum types (pass in the name of the enum member, and display options in the help)
|
|
33
|
+
- Custom post-processing (post_process method)
|
|
34
|
+
|
|
35
|
+
Detailed introduction is coming soon.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SimpleArgParser
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
## Introduction
|
|
6
|
+
|
|
7
|
+
This is a simple command line argument parser encapsulated based on Python dataclasses and type hints, supporting:
|
|
8
|
+
- Defining arguments using classes (required, optional, and arguments with default values)
|
|
9
|
+
- Nested dataclasses, with argument names separated by dots
|
|
10
|
+
- JSON configuration file loading (priority: command line > code input > JSON config > default value)
|
|
11
|
+
- List type arguments (supports comma separation)
|
|
12
|
+
- Enum types (pass in the name of the enum member, and display options in the help)
|
|
13
|
+
- Custom post-processing (post_process method)
|
|
14
|
+
|
|
15
|
+
Detailed introduction is coming soon.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="simpleArgParser",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
author="raibows",
|
|
7
|
+
author_email="raibows@hotmail.com",
|
|
8
|
+
description="A simple typed argument parser using dataclasses and type hints. This project is largely generated by ChatGPT.",
|
|
9
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
10
|
+
long_description_content_type="text/markdown",
|
|
11
|
+
url="https://github.com/Raibows/SimpleArgParser",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
python_requires=">=3.8",
|
|
14
|
+
classifiers=[
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
],
|
|
18
|
+
entry_points={
|
|
19
|
+
"console_scripts": [
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import dataclasses
|
|
3
|
+
from dataclasses import fields, MISSING, asdict
|
|
4
|
+
from typing import Optional, Union, get_origin, get_args, Type, List
|
|
5
|
+
import enum
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
import inspect
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# Global sentinel
|
|
13
|
+
NOT_PROVIDED = object()
|
|
14
|
+
|
|
15
|
+
class SpecialLoadMarker:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def bool_converter(s):
|
|
19
|
+
"""Supports case-insensitive yes/no, true/false conversion to boolean values"""
|
|
20
|
+
if isinstance(s, bool):
|
|
21
|
+
return s
|
|
22
|
+
lower = s.lower()
|
|
23
|
+
if lower in ("yes", "true", "t", "y", "1"):
|
|
24
|
+
return True
|
|
25
|
+
elif lower in ("no", "false", "f", "n", "0"):
|
|
26
|
+
return False
|
|
27
|
+
else:
|
|
28
|
+
raise argparse.ArgumentTypeError(f"Invalid boolean value: {s}")
|
|
29
|
+
|
|
30
|
+
def extract_field_comments(cls: Type) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
Extracts comments above fields from the class's source code, returns a dictionary of {field_name: comment_content}.
|
|
33
|
+
Only effective when source code is accessible.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
source = inspect.getsource(cls)
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
lines = source.splitlines()
|
|
40
|
+
field_pattern = re.compile(r'^\s*(\w+)\s*:')
|
|
41
|
+
field_help = {}
|
|
42
|
+
current_comments = []
|
|
43
|
+
for line in lines:
|
|
44
|
+
stripped = line.strip()
|
|
45
|
+
if stripped.startswith('#'):
|
|
46
|
+
comment_text = stripped.lstrip('#').strip()
|
|
47
|
+
current_comments.append(comment_text)
|
|
48
|
+
else:
|
|
49
|
+
m = field_pattern.match(line)
|
|
50
|
+
if m:
|
|
51
|
+
field_name = m.group(1)
|
|
52
|
+
if current_comments:
|
|
53
|
+
field_help[field_name] = " ".join(current_comments)
|
|
54
|
+
current_comments = []
|
|
55
|
+
else:
|
|
56
|
+
current_comments = []
|
|
57
|
+
return field_help
|
|
58
|
+
|
|
59
|
+
def get_by_path(d: dict, path: str):
|
|
60
|
+
"""Retrieve a value from a nested dictionary d based on a dot-separated path"""
|
|
61
|
+
parts = path.split('.')
|
|
62
|
+
current = d
|
|
63
|
+
for p in parts:
|
|
64
|
+
if isinstance(current, dict) and p in current:
|
|
65
|
+
current = current[p]
|
|
66
|
+
else:
|
|
67
|
+
return None
|
|
68
|
+
return current
|
|
69
|
+
|
|
70
|
+
def remove_by_path(d: dict, path: str):
|
|
71
|
+
"""Remove a key from a nested dictionary d based on a dot-separated path"""
|
|
72
|
+
parts = path.split('.')
|
|
73
|
+
current = d
|
|
74
|
+
for p in parts[:-1]:
|
|
75
|
+
if p in current:
|
|
76
|
+
current = current[p]
|
|
77
|
+
else:
|
|
78
|
+
return
|
|
79
|
+
current.pop(parts[-1], None)
|
|
80
|
+
|
|
81
|
+
def convert_type(typ: Type):
|
|
82
|
+
"""
|
|
83
|
+
Returns a conversion function. For bool type, uses custom bool_converter; for Enum types,
|
|
84
|
+
matches based on enum member name; otherwise returns typ itself.
|
|
85
|
+
"""
|
|
86
|
+
if typ is bool:
|
|
87
|
+
return bool_converter
|
|
88
|
+
if isinstance(typ, type) and issubclass(typ, enum.Enum):
|
|
89
|
+
return lambda s: typ[s]
|
|
90
|
+
return typ
|
|
91
|
+
|
|
92
|
+
def convert_value(value, target_type: Type):
|
|
93
|
+
"""Converts value to target_type, supports bool, Enum, list and basic types"""
|
|
94
|
+
if get_origin(target_type) in (Union, types.UnionType):
|
|
95
|
+
non_none = [a for a in get_args(target_type) if a is not type(None)]
|
|
96
|
+
if len(non_none) == 1:
|
|
97
|
+
target_type = non_none[0]
|
|
98
|
+
if target_type is bool:
|
|
99
|
+
return bool_converter(value) if isinstance(value, str) else bool(value)
|
|
100
|
+
if isinstance(target_type, type) and issubclass(target_type, enum.Enum):
|
|
101
|
+
if isinstance(value, str):
|
|
102
|
+
return target_type[value]
|
|
103
|
+
return target_type(value)
|
|
104
|
+
if get_origin(target_type) is list:
|
|
105
|
+
inner_type = get_args(target_type)[0]
|
|
106
|
+
if isinstance(value, str):
|
|
107
|
+
return [convert_value(item.strip(), inner_type) for item in value.split(',')]
|
|
108
|
+
elif isinstance(value, list):
|
|
109
|
+
return [convert_value(item, inner_type) for item in value]
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Cannot convert {value} to {target_type}")
|
|
112
|
+
try:
|
|
113
|
+
return target_type(value)
|
|
114
|
+
except Exception:
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
def nest_namespace(ns: dict) -> dict:
|
|
118
|
+
"""Convert a flat argparse namespace to a nested dictionary (based on dot-separated names)"""
|
|
119
|
+
nested = {}
|
|
120
|
+
for k, v in ns.items():
|
|
121
|
+
parts = k.split('.')
|
|
122
|
+
current = nested
|
|
123
|
+
for part in parts[:-1]:
|
|
124
|
+
current = current.setdefault(part, {})
|
|
125
|
+
current[parts[-1]] = v
|
|
126
|
+
return nested
|
|
127
|
+
|
|
128
|
+
def deep_merge(a: dict, b: dict) -> dict:
|
|
129
|
+
"""
|
|
130
|
+
Recursively merge dictionaries, b's values override a's values, but if the value in b is NOT_PROVIDED,
|
|
131
|
+
then preserve the valid values already in a.
|
|
132
|
+
"""
|
|
133
|
+
result = dict(a)
|
|
134
|
+
for k, v in b.items():
|
|
135
|
+
# If b's value is NOT_PROVIDED, don't override a's value
|
|
136
|
+
if v is NOT_PROVIDED:
|
|
137
|
+
continue
|
|
138
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
139
|
+
result[k] = deep_merge(result[k], v)
|
|
140
|
+
else:
|
|
141
|
+
result[k] = v
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
def fill_defaults(d: dict, cls: Type) -> dict:
|
|
145
|
+
"""
|
|
146
|
+
Fill in dataclass default values, and check if required fields are missing.
|
|
147
|
+
If required fields are missing, raise an error.
|
|
148
|
+
"""
|
|
149
|
+
result = dict(d)
|
|
150
|
+
for f in fields(cls):
|
|
151
|
+
if f.name not in result or result[f.name] is NOT_PROVIDED:
|
|
152
|
+
if f.default is MISSING and f.default_factory is MISSING:
|
|
153
|
+
raise ValueError(f"Missing required parameter: {f.name}")
|
|
154
|
+
elif f.default is not MISSING:
|
|
155
|
+
result[f.name] = f.default
|
|
156
|
+
elif f.default_factory is not MISSING:
|
|
157
|
+
result[f.name] = f.default_factory()
|
|
158
|
+
else:
|
|
159
|
+
if dataclasses.is_dataclass(f.type) and isinstance(result[f.name], dict):
|
|
160
|
+
result[f.name] = fill_defaults(result[f.name], f.type)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
def from_dict(cls: Type, d: dict):
|
|
164
|
+
"""Construct a dataclass instance from a dictionary, supports nested dataclasses"""
|
|
165
|
+
kwargs = {}
|
|
166
|
+
for f in fields(cls):
|
|
167
|
+
if dataclasses.is_dataclass(f.type):
|
|
168
|
+
sub_dict = d.get(f.name, {})
|
|
169
|
+
kwargs[f.name] = from_dict(f.type, sub_dict)
|
|
170
|
+
else:
|
|
171
|
+
if f.name in d:
|
|
172
|
+
kwargs[f.name] = convert_value(d[f.name], f.type)
|
|
173
|
+
return cls(**kwargs)
|
|
174
|
+
|
|
175
|
+
def add_arguments_from_dataclass(parser: argparse.ArgumentParser, cls: Type, prefix: str = "", special_fields: set = None):
|
|
176
|
+
"""
|
|
177
|
+
Recursively add command line arguments based on dataclass definition:
|
|
178
|
+
- Nested fields use dot notation for parameter names;
|
|
179
|
+
- Supports list, Enum, bool types, etc.;
|
|
180
|
+
- Automatically captures comments above fields;
|
|
181
|
+
- If a field's default value is special_load() (a SpecialLoadMarker instance), records the complete path of that field in the special_fields set.
|
|
182
|
+
"""
|
|
183
|
+
if special_fields is None:
|
|
184
|
+
special_fields = set()
|
|
185
|
+
field_help_map = extract_field_comments(cls)
|
|
186
|
+
for f in fields(cls):
|
|
187
|
+
field_type = f.type
|
|
188
|
+
if get_origin(field_type) in (Union, types.UnionType):
|
|
189
|
+
non_none = [a for a in get_args(field_type) if a is not type(None)]
|
|
190
|
+
if len(non_none) == 1:
|
|
191
|
+
field_type = non_none[0]
|
|
192
|
+
full_field_name = f"{prefix}{f.name}"
|
|
193
|
+
if dataclasses.is_dataclass(field_type):
|
|
194
|
+
new_prefix = f"{full_field_name}."
|
|
195
|
+
add_arguments_from_dataclass(parser, field_type, prefix=new_prefix, special_fields=special_fields)
|
|
196
|
+
else:
|
|
197
|
+
arg_name = f"--{full_field_name}"
|
|
198
|
+
dest_name = full_field_name
|
|
199
|
+
if get_origin(field_type) is list:
|
|
200
|
+
inner_type = get_args(field_type)[0]
|
|
201
|
+
def list_converter(s, inner_type=inner_type):
|
|
202
|
+
return [convert_value(item.strip(), inner_type) for item in s.split(',')]
|
|
203
|
+
conv_type = list_converter
|
|
204
|
+
else:
|
|
205
|
+
conv_type = convert_type(field_type)
|
|
206
|
+
extra_help = field_help_map.get(f.name, "")
|
|
207
|
+
help_text = f"{extra_help} (type: {field_type})".strip()
|
|
208
|
+
kwargs = {
|
|
209
|
+
"dest": dest_name,
|
|
210
|
+
"type": conv_type,
|
|
211
|
+
"help": help_text
|
|
212
|
+
}
|
|
213
|
+
if isinstance(field_type, type) and issubclass(field_type, enum.Enum):
|
|
214
|
+
kwargs["choices"] = list(field_type.__members__.keys())
|
|
215
|
+
# If the field's default value is special_load() (a SpecialLoadMarker instance), record the field's path
|
|
216
|
+
if isinstance(f.default, SpecialLoadMarker):
|
|
217
|
+
special_fields.add(full_field_name)
|
|
218
|
+
if f.default is MISSING and f.default_factory is MISSING:
|
|
219
|
+
kwargs["required"] = True
|
|
220
|
+
else:
|
|
221
|
+
kwargs["default"] = NOT_PROVIDED
|
|
222
|
+
default_val = f.default if f.default is not MISSING else f.default_factory()
|
|
223
|
+
kwargs["help"] += f" (default: {default_val})"
|
|
224
|
+
parser.add_argument(arg_name, **kwargs)
|
|
225
|
+
|
|
226
|
+
def recursive_post_process(obj):
|
|
227
|
+
"""
|
|
228
|
+
Recursively call the process_args or post_process method of a dataclass object, executed from top to bottom.
|
|
229
|
+
If the object defines process_args, call it first; otherwise call post_process.
|
|
230
|
+
"""
|
|
231
|
+
if dataclasses.is_dataclass(obj):
|
|
232
|
+
if hasattr(obj, "process_args") and callable(obj.process_args):
|
|
233
|
+
obj.process_args()
|
|
234
|
+
elif hasattr(obj, "post_process") and callable(obj.post_process):
|
|
235
|
+
obj.post_process()
|
|
236
|
+
for field in fields(obj):
|
|
237
|
+
value = getattr(obj, field.name)
|
|
238
|
+
if dataclasses.is_dataclass(value):
|
|
239
|
+
recursive_post_process(value)
|
|
240
|
+
|
|
241
|
+
def parse_args(cls: Type, pass_in: List[str] = None):
|
|
242
|
+
"""
|
|
243
|
+
Parse command line arguments, supporting:
|
|
244
|
+
- Code-provided arguments pass_in merged with sys.argv (command line arguments take priority);
|
|
245
|
+
- JSON configuration file loading: if a field's default value is special_load() (SpecialLoadMarker instance), and the user provides a non-empty string,
|
|
246
|
+
then try to load that string as a JSON file path and merge with other configurations;
|
|
247
|
+
- Merge default values (and check required fields);
|
|
248
|
+
- Recursively call post-processing methods (process_args/post_process) for all dataclasses;
|
|
249
|
+
- If --help/-h is detected, directly print help information and exit.
|
|
250
|
+
Priority: command line > code input > specially loaded JSON config > default values.
|
|
251
|
+
"""
|
|
252
|
+
code_args = pass_in if pass_in is not None else []
|
|
253
|
+
cmd_args = sys.argv[1:]
|
|
254
|
+
args_list = code_args + cmd_args
|
|
255
|
+
|
|
256
|
+
if any(arg in ('-h', '--help') for arg in args_list):
|
|
257
|
+
full_parser = argparse.ArgumentParser()
|
|
258
|
+
add_arguments_from_dataclass(full_parser, cls)
|
|
259
|
+
full_parser.print_help()
|
|
260
|
+
sys.exit(0)
|
|
261
|
+
|
|
262
|
+
special_fields = set()
|
|
263
|
+
parser = argparse.ArgumentParser()
|
|
264
|
+
add_arguments_from_dataclass(parser, cls, special_fields=special_fields)
|
|
265
|
+
args = parser.parse_args(args_list)
|
|
266
|
+
flat_ns = vars(args)
|
|
267
|
+
nested_args = nest_namespace(flat_ns)
|
|
268
|
+
|
|
269
|
+
# If special load fields exist, ensure there's at most one
|
|
270
|
+
if len(special_fields) > 1:
|
|
271
|
+
raise ValueError(f"At most one special load field is allowed, found: {special_fields}")
|
|
272
|
+
if special_fields:
|
|
273
|
+
special_field_path = next(iter(special_fields))
|
|
274
|
+
special_value = get_by_path(nested_args, special_field_path)
|
|
275
|
+
if isinstance(special_value, str) and special_value.strip():
|
|
276
|
+
try:
|
|
277
|
+
with open(special_value, 'r') as f:
|
|
278
|
+
json_special = json.load(f)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(f"Error loading JSON config from {special_value}: {e}", file=sys.stderr)
|
|
281
|
+
json_special = {}
|
|
282
|
+
# Remove the special field and merge the loaded JSON into the configuration (command line arguments have higher priority)
|
|
283
|
+
remove_by_path(nested_args, special_field_path)
|
|
284
|
+
nested_args = deep_merge(json_special, nested_args)
|
|
285
|
+
final_dict = fill_defaults(nested_args, cls)
|
|
286
|
+
config = from_dict(cls, final_dict)
|
|
287
|
+
recursive_post_process(config)
|
|
288
|
+
return config
|
|
289
|
+
|
|
290
|
+
def to_json(config) -> str:
|
|
291
|
+
"""Convert a dataclass instance to a JSON string (Enum types are converted to their names)"""
|
|
292
|
+
def default(o):
|
|
293
|
+
if isinstance(o, enum.Enum):
|
|
294
|
+
return o.name
|
|
295
|
+
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
|
|
296
|
+
return json.dumps(asdict(config), indent=4, default=default)
|
|
297
|
+
|
|
298
|
+
def main():
|
|
299
|
+
print("simpleArgParser: Please use parse_args() in your code to parse configuration.")
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: simpleArgParser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple typed argument parser using dataclasses and type hints. This project is largely generated by ChatGPT.
|
|
5
|
+
Home-page: https://github.com/Raibows/SimpleArgParser
|
|
6
|
+
Author: raibows
|
|
7
|
+
Author-email: raibows@hotmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# SimpleArgParser
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
This is a simple command line argument parser encapsulated based on Python dataclasses and type hints, supporting:
|
|
28
|
+
- Defining arguments using classes (required, optional, and arguments with default values)
|
|
29
|
+
- Nested dataclasses, with argument names separated by dots
|
|
30
|
+
- JSON configuration file loading (priority: command line > code input > JSON config > default value)
|
|
31
|
+
- List type arguments (supports comma separation)
|
|
32
|
+
- Enum types (pass in the name of the enum member, and display options in the help)
|
|
33
|
+
- Custom post-processing (post_process method)
|
|
34
|
+
|
|
35
|
+
Detailed introduction is coming soon.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simpleArgParser
|