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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .s_argparse import (
4
+ parse_args,
5
+ to_json,
6
+ SpecialLoadMarker,
7
+ )
8
+
9
+ __all__ = ["parse_args", "SpecialLoadMarker", "to_json"]
@@ -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,8 @@
1
+ README.md
2
+ setup.py
3
+ simpleArgParser/__init__.py
4
+ simpleArgParser/s_argparse.py
5
+ simpleArgParser.egg-info/PKG-INFO
6
+ simpleArgParser.egg-info/SOURCES.txt
7
+ simpleArgParser.egg-info/dependency_links.txt
8
+ simpleArgParser.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ simpleArgParser