chatterer 0.1.12__py3-none-any.whl → 0.1.14__py3-none-any.whl
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.
- chatterer/__init__.py +41 -4
- chatterer/common_types/__init__.py +21 -0
- chatterer/common_types/io.py +19 -0
- chatterer/interactive.py +353 -0
- chatterer/language_model.py +129 -252
- chatterer/messages.py +13 -1
- chatterer/tools/__init__.py +27 -9
- chatterer/tools/{webpage_to_markdown/utils.py → caption_markdown_images.py} +158 -108
- chatterer/tools/convert_pdf_to_markdown.py +302 -0
- chatterer/tools/convert_to_text.py +49 -65
- chatterer/tools/upstage_document_parser.py +705 -0
- chatterer/tools/{webpage_to_markdown/playwright_bot.py → webpage_to_markdown.py} +197 -107
- chatterer/tools/youtube.py +2 -1
- chatterer/utils/__init__.py +4 -1
- chatterer/utils/{image.py → base64_image.py} +56 -62
- chatterer/utils/bytesio.py +59 -0
- chatterer/utils/cli.py +476 -0
- chatterer/utils/code_agent.py +137 -38
- chatterer/utils/imghdr.py +148 -0
- chatterer-0.1.14.dist-info/METADATA +387 -0
- chatterer-0.1.14.dist-info/RECORD +34 -0
- chatterer/tools/webpage_to_markdown/__init__.py +0 -4
- chatterer-0.1.12.dist-info/METADATA +0 -170
- chatterer-0.1.12.dist-info/RECORD +0 -27
- {chatterer-0.1.12.dist-info → chatterer-0.1.14.dist-info}/WHEEL +0 -0
- {chatterer-0.1.12.dist-info → chatterer-0.1.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
import os
|
2
|
+
from contextlib import contextmanager, suppress
|
3
|
+
from io import BytesIO
|
4
|
+
from typing import Iterator, Optional
|
5
|
+
|
6
|
+
from ..common_types.io import BytesReadable, PathOrReadable, StringReadable
|
7
|
+
|
8
|
+
|
9
|
+
@contextmanager
|
10
|
+
def read_bytes_stream(
|
11
|
+
path_or_file: PathOrReadable,
|
12
|
+
assume_pathlike_bytes_as_path: bool = False,
|
13
|
+
assume_pathlike_string_as_path: bool = True,
|
14
|
+
) -> Iterator[Optional[BytesReadable]]:
|
15
|
+
"""
|
16
|
+
Context manager for opening a file or using an existing stream.
|
17
|
+
|
18
|
+
Handles different types of input (file paths, byte streams, string streams)
|
19
|
+
and yields a BytesReadable object that can be used to read binary data.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
path_or_file: File path or readable object.
|
23
|
+
assume_pathlike_bytes_as_path: If True, assume bytes-like objects are file paths. Else, treat as data itself.
|
24
|
+
assume_pathlike_string_as_path: If True, assume string-like objects are file paths. Else, treat as data itself.
|
25
|
+
|
26
|
+
Yields:
|
27
|
+
Optional[BytesReadable]: A readable binary stream or None if opening fails.
|
28
|
+
"""
|
29
|
+
stream: Optional[BytesReadable] = None
|
30
|
+
should_close: bool = True # Whether the stream should be closed after use
|
31
|
+
try:
|
32
|
+
with suppress(BaseException):
|
33
|
+
if isinstance(path_or_file, BytesReadable):
|
34
|
+
# Assume the input is already a bytes stream
|
35
|
+
# NOTE: Delivers itself, so shouldn't be closed.
|
36
|
+
stream = path_or_file
|
37
|
+
should_close = False
|
38
|
+
elif isinstance(path_or_file, StringReadable):
|
39
|
+
# Convert the string stream to bytes stream
|
40
|
+
stream = BytesIO(path_or_file.read().encode("utf-8"))
|
41
|
+
elif isinstance(path_or_file, bytes):
|
42
|
+
# Convert the bytes-like object to bytes stream
|
43
|
+
if assume_pathlike_bytes_as_path and os.path.exists(path_or_file):
|
44
|
+
stream = open(path_or_file, "rb")
|
45
|
+
else:
|
46
|
+
stream = BytesIO(path_or_file)
|
47
|
+
elif isinstance(path_or_file, str):
|
48
|
+
# Convert the file path to bytes stream
|
49
|
+
if assume_pathlike_string_as_path and os.path.exists(path_or_file):
|
50
|
+
stream = open(path_or_file, "rb")
|
51
|
+
else:
|
52
|
+
stream = BytesIO(path_or_file.encode("utf-8"))
|
53
|
+
else:
|
54
|
+
# Assume the input is a file descriptor or path
|
55
|
+
stream = open(path_or_file, "rb")
|
56
|
+
yield stream
|
57
|
+
finally:
|
58
|
+
if stream is not None and should_close:
|
59
|
+
stream.close()
|
chatterer/utils/cli.py
ADDED
@@ -0,0 +1,476 @@
|
|
1
|
+
import argparse
|
2
|
+
import io
|
3
|
+
import typing
|
4
|
+
import warnings
|
5
|
+
from dataclasses import dataclass, field, fields
|
6
|
+
from typing import (
|
7
|
+
IO,
|
8
|
+
Callable,
|
9
|
+
Dict,
|
10
|
+
Generic,
|
11
|
+
Iterable,
|
12
|
+
List,
|
13
|
+
Literal,
|
14
|
+
NamedTuple,
|
15
|
+
Optional,
|
16
|
+
Sequence,
|
17
|
+
Tuple,
|
18
|
+
TypeVar,
|
19
|
+
Union,
|
20
|
+
)
|
21
|
+
|
22
|
+
# --- Type Definitions ---
|
23
|
+
SUPPRESS_LITERAL_TYPE = Literal["==SUPPRESS=="]
|
24
|
+
SUPPRESS: SUPPRESS_LITERAL_TYPE = "==SUPPRESS=="
|
25
|
+
ACTION_TYPES_THAT_DONT_SUPPORT_TYPE_KWARG = (
|
26
|
+
"store_const",
|
27
|
+
"store_true",
|
28
|
+
"store_false",
|
29
|
+
"append_const",
|
30
|
+
"count",
|
31
|
+
"help",
|
32
|
+
"version",
|
33
|
+
)
|
34
|
+
Action = Optional[
|
35
|
+
Literal[
|
36
|
+
"store",
|
37
|
+
"store_const",
|
38
|
+
"store_true",
|
39
|
+
"store_false",
|
40
|
+
"append",
|
41
|
+
"append_const",
|
42
|
+
"count",
|
43
|
+
"help",
|
44
|
+
"version",
|
45
|
+
"extend",
|
46
|
+
]
|
47
|
+
]
|
48
|
+
T = TypeVar("T")
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class ArgumentSpec(Generic[T]):
|
53
|
+
"""Represents the specification for a command-line argument."""
|
54
|
+
|
55
|
+
name_or_flags: List[str]
|
56
|
+
action: Action = None
|
57
|
+
nargs: Optional[Union[int, Literal["*", "+", "?"]]] = None
|
58
|
+
const: Optional[object] = None
|
59
|
+
default: Optional[Union[T, SUPPRESS_LITERAL_TYPE]] = None
|
60
|
+
choices: Optional[Sequence[T]] = None
|
61
|
+
required: bool = False
|
62
|
+
help: str = ""
|
63
|
+
metavar: Optional[str] = None
|
64
|
+
version: Optional[str] = None
|
65
|
+
type: Optional[Union[Callable[[str], T], argparse.FileType]] = None
|
66
|
+
value: Optional[T] = field(init=False, default=None) # Parsed value stored here
|
67
|
+
|
68
|
+
@property
|
69
|
+
def value_not_none(self) -> T:
|
70
|
+
"""Returns the value, raising an error if it's None."""
|
71
|
+
if self.value is None:
|
72
|
+
raise ValueError(f"Value for {self.name_or_flags} is None.")
|
73
|
+
return self.value
|
74
|
+
|
75
|
+
def get_add_argument_kwargs(self) -> Dict[str, object]:
|
76
|
+
"""Prepares keyword arguments for argparse.ArgumentParser.add_argument."""
|
77
|
+
kwargs: Dict[str, object] = {}
|
78
|
+
argparse_fields: set[str] = {f.name for f in fields(self) if f.name not in ("name_or_flags", "value")}
|
79
|
+
for field_name in argparse_fields:
|
80
|
+
attr_value = getattr(self, field_name)
|
81
|
+
if field_name == "default":
|
82
|
+
if attr_value is None:
|
83
|
+
pass # Keep default=None if explicitly set or inferred
|
84
|
+
elif attr_value in get_args(SUPPRESS_LITERAL_TYPE):
|
85
|
+
kwargs[field_name] = argparse.SUPPRESS
|
86
|
+
else:
|
87
|
+
kwargs[field_name] = attr_value
|
88
|
+
elif attr_value is not None:
|
89
|
+
if field_name == "type" and self.action in ACTION_TYPES_THAT_DONT_SUPPORT_TYPE_KWARG:
|
90
|
+
continue
|
91
|
+
kwargs[field_name] = attr_value
|
92
|
+
return kwargs
|
93
|
+
|
94
|
+
|
95
|
+
class ArgumentSpecType(NamedTuple):
|
96
|
+
T: object # The T in ArgumentSpec[T]
|
97
|
+
element_type: typing.Optional[typing.Type[object]] # The E in ArgumentSpec[List[E]] or ArgumentSpec[Tuple[E]]
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def from_hint(cls, hints: Dict[str, object], attr_name: str):
|
101
|
+
if (
|
102
|
+
(hint := hints.get(attr_name))
|
103
|
+
and (hint_origin := get_origin(hint))
|
104
|
+
and (hint_args := get_args(hint))
|
105
|
+
and isinstance(hint_origin, type)
|
106
|
+
and issubclass(hint_origin, ArgumentSpec)
|
107
|
+
):
|
108
|
+
T: object = hint_args[0] # Extract T
|
109
|
+
element_type: typing.Optional[object] = None
|
110
|
+
if isinstance(outer_origin := get_origin(T), type):
|
111
|
+
if issubclass(outer_origin, list) and (args := get_args(T)):
|
112
|
+
element_type = args[0] # Extract E
|
113
|
+
elif issubclass(outer_origin, tuple) and (args := get_args(T)):
|
114
|
+
element_type = args # Extract E
|
115
|
+
else:
|
116
|
+
element_type = None # The E in ArgumentSpec[List[E]] or Tuple[E, ...]
|
117
|
+
if not isinstance(element_type, type):
|
118
|
+
element_type = None
|
119
|
+
return cls(T=T, element_type=element_type)
|
120
|
+
|
121
|
+
@property
|
122
|
+
def choices(self) -> typing.Optional[Tuple[object, ...]]:
|
123
|
+
# ArgumentSpec[Literal["A", "B"]] or ArgumentSpec[List[Literal["A", "B"]]]
|
124
|
+
T_origin = get_origin(self.T)
|
125
|
+
if (
|
126
|
+
isinstance(T_origin, type)
|
127
|
+
and (issubclass(T_origin, (list, tuple)))
|
128
|
+
and (args := get_args(self.T))
|
129
|
+
and (get_origin(arg := args[0]) is typing.Literal)
|
130
|
+
and (literals := get_args(arg))
|
131
|
+
):
|
132
|
+
return literals
|
133
|
+
elif T_origin is typing.Literal and (args := get_args(self.T)):
|
134
|
+
return args
|
135
|
+
|
136
|
+
@property
|
137
|
+
def type(self) -> typing.Optional[typing.Type[object]]:
|
138
|
+
if self.element_type is not None:
|
139
|
+
return self.element_type # If it's List[E] or Sequence[E], use E as type
|
140
|
+
elif self.T and isinstance(self.T, type):
|
141
|
+
return self.T # Use T as type
|
142
|
+
|
143
|
+
@property
|
144
|
+
def should_return_as_list(self) -> bool:
|
145
|
+
"""Determines if the argument should be returned as a list."""
|
146
|
+
T_origin = get_origin(self.T)
|
147
|
+
if isinstance(T_origin, type):
|
148
|
+
if issubclass(T_origin, list):
|
149
|
+
return True
|
150
|
+
return False
|
151
|
+
|
152
|
+
@property
|
153
|
+
def should_return_as_tuple(self) -> bool:
|
154
|
+
"""Determines if the argument should be returned as a tuple."""
|
155
|
+
T_origin = get_origin(self.T)
|
156
|
+
if isinstance(T_origin, type):
|
157
|
+
if issubclass(T_origin, tuple):
|
158
|
+
return True
|
159
|
+
return False
|
160
|
+
|
161
|
+
@property
|
162
|
+
def tuple_nargs(self) -> Optional[int]:
|
163
|
+
if self.should_return_as_tuple and (args := get_args(self.T)) and Ellipsis not in args:
|
164
|
+
return len(args)
|
165
|
+
|
166
|
+
|
167
|
+
class BaseArguments:
|
168
|
+
"""Base class for defining arguments declaratively using ArgumentSpec."""
|
169
|
+
|
170
|
+
__argspec__: Dict[str, ArgumentSpec[object]]
|
171
|
+
__argspectype__: Dict[str, ArgumentSpecType]
|
172
|
+
|
173
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
174
|
+
"""
|
175
|
+
Processes ArgumentSpec definitions in subclasses upon class creation.
|
176
|
+
Automatically infers 'type' and 'choices' from type hints if possible.
|
177
|
+
"""
|
178
|
+
super().__init_subclass__(**kwargs)
|
179
|
+
cls.__argspec__ = {}
|
180
|
+
cls.__argspectype__ = {}
|
181
|
+
for current_cls in reversed(cls.__mro__):
|
182
|
+
if current_cls is object or current_cls is BaseArguments:
|
183
|
+
continue
|
184
|
+
current_vars = vars(current_cls)
|
185
|
+
try:
|
186
|
+
hints: Dict[str, object] = typing.get_type_hints(current_cls, globalns=dict(current_vars))
|
187
|
+
for attr_name, attr_value in current_vars.items():
|
188
|
+
if isinstance(attr_value, ArgumentSpec):
|
189
|
+
attr_value = typing.cast(ArgumentSpec[object], attr_value)
|
190
|
+
if arguments_spec_type := ArgumentSpecType.from_hint(hints=hints, attr_name=attr_name):
|
191
|
+
cls.__argspectype__[attr_name] = arguments_spec_type
|
192
|
+
if attr_value.choices is None and (literals := arguments_spec_type.choices):
|
193
|
+
attr_value.choices = literals
|
194
|
+
if attr_value.type is None and (type := arguments_spec_type.type):
|
195
|
+
attr_value.type = type
|
196
|
+
if tuple_nargs := arguments_spec_type.tuple_nargs:
|
197
|
+
attr_value.nargs = tuple_nargs
|
198
|
+
cls.__argspec__[attr_name] = attr_value
|
199
|
+
except Exception as e:
|
200
|
+
warnings.warn(f"Could not fully analyze type hints for {current_cls.__name__}: {e}", stacklevel=2)
|
201
|
+
for attr_name, attr_value in current_vars.items():
|
202
|
+
if isinstance(attr_value, ArgumentSpec) and attr_name not in cls.__argspec__:
|
203
|
+
cls.__argspec__[attr_name] = attr_value
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def iter_specs(cls) -> Iterable[Tuple[str, ArgumentSpec[object]]]:
|
207
|
+
"""Iterates over the registered (attribute_name, ArgumentSpec) pairs."""
|
208
|
+
yield from cls.__argspec__.items()
|
209
|
+
|
210
|
+
@classmethod
|
211
|
+
def get_parser(cls) -> argparse.ArgumentParser:
|
212
|
+
"""Creates and configures an ArgumentParser based on the defined ArgumentSpecs."""
|
213
|
+
arg_parser = argparse.ArgumentParser(
|
214
|
+
description=cls.__doc__, # Use class docstring as description
|
215
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
216
|
+
add_help=False, # Add custom help argument later
|
217
|
+
)
|
218
|
+
# Add standard help argument
|
219
|
+
arg_parser.add_argument(
|
220
|
+
"-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit."
|
221
|
+
)
|
222
|
+
# Add arguments to the parser based on registered ArgumentSpecs
|
223
|
+
for key, spec in cls.iter_specs():
|
224
|
+
kwargs = spec.get_add_argument_kwargs()
|
225
|
+
# Determine if it's a positional or optional argument
|
226
|
+
is_positional: bool = not any(name.startswith("-") for name in spec.name_or_flags)
|
227
|
+
if is_positional:
|
228
|
+
# For positional args: remove 'required' (implicit), let argparse derive 'dest'
|
229
|
+
kwargs.pop("required", None)
|
230
|
+
try:
|
231
|
+
arg_parser.add_argument(*spec.name_or_flags, **kwargs) # pyright: ignore[reportArgumentType]
|
232
|
+
except Exception as e:
|
233
|
+
# Provide informative error message
|
234
|
+
raise ValueError(
|
235
|
+
f"Error adding positional argument '{key}' with spec {spec.name_or_flags} and kwargs {kwargs}: {e}"
|
236
|
+
) from e
|
237
|
+
else: # Optional argument
|
238
|
+
try:
|
239
|
+
# For optional args: explicitly set 'dest' to the attribute name ('key')
|
240
|
+
arg_parser.add_argument(*spec.name_or_flags, dest=key, **kwargs) # pyright: ignore[reportArgumentType]
|
241
|
+
except Exception as e:
|
242
|
+
# Provide informative error message
|
243
|
+
raise ValueError(
|
244
|
+
f"Error adding optional argument '{key}' with spec {spec.name_or_flags} and kwargs {kwargs}: {e}"
|
245
|
+
) from e
|
246
|
+
return arg_parser
|
247
|
+
|
248
|
+
@classmethod
|
249
|
+
def load(cls, args: Optional[Sequence[str]] = None) -> None:
|
250
|
+
"""
|
251
|
+
Parses command-line arguments and assigns the values to the corresponding ArgumentSpec instances.
|
252
|
+
If 'args' is None, uses sys.argv[1:].
|
253
|
+
"""
|
254
|
+
parser = cls.get_parser()
|
255
|
+
try:
|
256
|
+
parsed_args = parser.parse_args(args)
|
257
|
+
except SystemExit as e:
|
258
|
+
# Allow SystemExit (e.g., from --help) to propagate
|
259
|
+
raise e
|
260
|
+
# Assign parsed values from the namespace
|
261
|
+
cls.load_from_namespace(parsed_args)
|
262
|
+
|
263
|
+
@classmethod
|
264
|
+
def load_from_namespace(cls, args: argparse.Namespace) -> None:
|
265
|
+
"""Assigns values from a parsed argparse.Namespace object to the ArgumentSpecs."""
|
266
|
+
for key, spec in cls.iter_specs():
|
267
|
+
# Determine the attribute name in the namespace
|
268
|
+
# Positional args use their name, optionals use the 'dest' (which is 'key')
|
269
|
+
is_positional = not any(name.startswith("-") for name in spec.name_or_flags)
|
270
|
+
attr_name = spec.name_or_flags[0] if is_positional else key
|
271
|
+
# Check if the attribute exists in the namespace
|
272
|
+
if not hasattr(args, attr_name):
|
273
|
+
continue
|
274
|
+
|
275
|
+
value: object = getattr(args, attr_name)
|
276
|
+
if value is argparse.SUPPRESS:
|
277
|
+
continue
|
278
|
+
|
279
|
+
# Assign the value unless it's the SUPPRESS sentinel
|
280
|
+
if argument_spec_type := cls.__argspectype__.get(key):
|
281
|
+
if argument_spec_type.should_return_as_list:
|
282
|
+
if isinstance(value, list):
|
283
|
+
value = typing.cast(List[object], value)
|
284
|
+
elif value is not None:
|
285
|
+
value = [value]
|
286
|
+
elif argument_spec_type.should_return_as_tuple:
|
287
|
+
if isinstance(value, tuple):
|
288
|
+
value = typing.cast(Tuple[object, ...], value)
|
289
|
+
elif value is not None:
|
290
|
+
if isinstance(value, list):
|
291
|
+
value = tuple(typing.cast(List[object], value))
|
292
|
+
else:
|
293
|
+
value = (value,)
|
294
|
+
spec.value = value
|
295
|
+
|
296
|
+
@classmethod
|
297
|
+
def get_value(cls, key: str) -> Optional[object]:
|
298
|
+
"""Retrieves the parsed value for a specific argument by its attribute name."""
|
299
|
+
if key in cls.__argspec__:
|
300
|
+
return cls.__argspec__[key].value
|
301
|
+
raise KeyError(f"Argument spec with key '{key}' not found.")
|
302
|
+
|
303
|
+
@classmethod
|
304
|
+
def get_all_values(cls) -> Dict[str, Optional[object]]:
|
305
|
+
"""Returns a dictionary of all argument attribute names and their parsed values."""
|
306
|
+
return {key: spec.value for key, spec in cls.iter_specs()}
|
307
|
+
|
308
|
+
def __init__(self) -> None:
|
309
|
+
self.load()
|
310
|
+
|
311
|
+
|
312
|
+
def get_args(t: object) -> Tuple[object, ...]:
|
313
|
+
"""Returns the arguments of a type or a generic type."""
|
314
|
+
return typing.get_args(t)
|
315
|
+
|
316
|
+
|
317
|
+
def get_origin(t: object) -> typing.Optional[object]:
|
318
|
+
"""Returns the origin of a type or a generic type."""
|
319
|
+
return typing.get_origin(t)
|
320
|
+
|
321
|
+
|
322
|
+
# --- Main execution block (Example Usage) ---
|
323
|
+
if __name__ == "__main__":
|
324
|
+
|
325
|
+
class __Arguments(BaseArguments):
|
326
|
+
"""Example argument parser demonstrating various features."""
|
327
|
+
|
328
|
+
my_str_arg: ArgumentSpec[str] = ArgumentSpec(
|
329
|
+
["-s", "--string-arg"], default="Hello", help="A string argument.", metavar="TEXT"
|
330
|
+
)
|
331
|
+
my_int_arg: ArgumentSpec[int] = ArgumentSpec(
|
332
|
+
["-i", "--integer-arg"], required=True, help="A required integer argument."
|
333
|
+
)
|
334
|
+
verbose: ArgumentSpec[bool] = ArgumentSpec(
|
335
|
+
["-v", "--verbose"], action="store_true", help="Increase output verbosity."
|
336
|
+
)
|
337
|
+
# --- List<str> ---
|
338
|
+
my_list_arg: ArgumentSpec[List[str]] = ArgumentSpec(
|
339
|
+
["--list-values"],
|
340
|
+
nargs="+",
|
341
|
+
help="One or more string values.",
|
342
|
+
default=None,
|
343
|
+
)
|
344
|
+
# --- Positional IO ---
|
345
|
+
input_file: ArgumentSpec[IO[str]] = ArgumentSpec(
|
346
|
+
["input_file"],
|
347
|
+
type=argparse.FileType("r", encoding="utf-8"),
|
348
|
+
help="Path to the input file (required).",
|
349
|
+
metavar="INPUT_PATH",
|
350
|
+
)
|
351
|
+
output_file: ArgumentSpec[Optional[IO[str]]] = ArgumentSpec(
|
352
|
+
["output_file"],
|
353
|
+
type=argparse.FileType("w", encoding="utf-8"),
|
354
|
+
nargs="?",
|
355
|
+
default=None,
|
356
|
+
help="Optional output file path.",
|
357
|
+
metavar="OUTPUT_PATH",
|
358
|
+
)
|
359
|
+
# --- Simple Literal (choices auto-detected) ---
|
360
|
+
log_level: ArgumentSpec[Literal["DEBUG", "INFO", "WARNING", "ERROR"]] = ArgumentSpec(
|
361
|
+
["--log-level"],
|
362
|
+
default="INFO",
|
363
|
+
help="Set the logging level.",
|
364
|
+
)
|
365
|
+
# --- Literal + explicit choices (explicit wins) ---
|
366
|
+
mode: ArgumentSpec[Literal["fast", "slow", "careful"]] = ArgumentSpec(
|
367
|
+
["--mode"],
|
368
|
+
choices=["fast", "slow"], # Explicit choices override Literal args
|
369
|
+
default="fast",
|
370
|
+
help="Operation mode.",
|
371
|
+
)
|
372
|
+
# --- List[Literal] (choices auto-detected) ---
|
373
|
+
enabled_features: ArgumentSpec[List[Literal["CACHE", "LOGGING", "RETRY"]]] = ArgumentSpec(
|
374
|
+
["--features"],
|
375
|
+
nargs="*", # 0 or more features
|
376
|
+
help="Enable specific features.",
|
377
|
+
default=[],
|
378
|
+
)
|
379
|
+
tuple_features: ArgumentSpec[
|
380
|
+
Tuple[Literal["CACHE", "LOGGING", "RETRY"], Literal["CAwCHE", "LOGGING", "RETRY"]]
|
381
|
+
] = ArgumentSpec(
|
382
|
+
["--tuple-features"],
|
383
|
+
help="Enable specific features (tuple).",
|
384
|
+
)
|
385
|
+
|
386
|
+
# --- SUPPRESS default ---
|
387
|
+
optional_flag: ArgumentSpec[str] = ArgumentSpec(
|
388
|
+
["--opt-flag"],
|
389
|
+
default=SUPPRESS,
|
390
|
+
help="An optional flag whose attribute might not be set.",
|
391
|
+
)
|
392
|
+
|
393
|
+
print("--- Initial State (Before Parsing) ---")
|
394
|
+
parser_for_debug = __Arguments.get_parser()
|
395
|
+
for k, s in __Arguments.iter_specs():
|
396
|
+
print(f"{k}: value={s.value}, type={s.type}, choices={s.choices}") # Check inferred choices
|
397
|
+
|
398
|
+
dummy_input_filename = "temp_input_for_argparse_test.txt"
|
399
|
+
try:
|
400
|
+
with open(dummy_input_filename, "w", encoding="utf-8") as f:
|
401
|
+
f.write("This is a test file.\n")
|
402
|
+
print(f"\nCreated dummy input file: {dummy_input_filename}")
|
403
|
+
except Exception as e:
|
404
|
+
print(f"Warning: Could not create dummy input file '{dummy_input_filename}': {e}")
|
405
|
+
|
406
|
+
# Example command-line arguments (Adjusted order)
|
407
|
+
test_args = [
|
408
|
+
dummy_input_filename,
|
409
|
+
"-i",
|
410
|
+
"42",
|
411
|
+
"--log-level",
|
412
|
+
"WARNING",
|
413
|
+
"--mode",
|
414
|
+
"slow",
|
415
|
+
"--list-values",
|
416
|
+
"apple",
|
417
|
+
"banana",
|
418
|
+
"--features",
|
419
|
+
"CACHE",
|
420
|
+
"RETRY", # Test List[Literal]
|
421
|
+
"--tuple-features",
|
422
|
+
"CACHE",
|
423
|
+
"LOGGING", # Test Tuple[Literal]
|
424
|
+
]
|
425
|
+
# test_args = ['--features', 'INVALID'] # Test invalid choice for List[Literal]
|
426
|
+
# test_args = ['-h']
|
427
|
+
|
428
|
+
try:
|
429
|
+
print(f"\n--- Loading Arguments (Args: {test_args if test_args else 'from sys.argv'}) ---")
|
430
|
+
__Arguments.load(test_args)
|
431
|
+
print("\n--- Final Loaded Arguments ---")
|
432
|
+
all_values = __Arguments.get_all_values()
|
433
|
+
for key, value in all_values.items():
|
434
|
+
value_type = type(value).__name__
|
435
|
+
if isinstance(value, io.IOBase):
|
436
|
+
try:
|
437
|
+
name = getattr(value, "name", "<unknown_name>")
|
438
|
+
mode = getattr(value, "mode", "?")
|
439
|
+
value_repr = f"<IO {name} mode='{mode}'>"
|
440
|
+
except ValueError:
|
441
|
+
value_repr = "<IO object (closed)>"
|
442
|
+
else:
|
443
|
+
value_repr = repr(value)
|
444
|
+
print(f"{key}: {value_repr} (Type: {value_type})")
|
445
|
+
|
446
|
+
print("\n--- Accessing Specific Values ---")
|
447
|
+
print(f"Features : {__Arguments.get_value('enabled_features')}") # Check List[Literal] value
|
448
|
+
|
449
|
+
input_f = __Arguments.get_value("input_file")
|
450
|
+
if isinstance(input_f, io.IOBase):
|
451
|
+
try:
|
452
|
+
print(f"\nReading from input file: {input_f.name}") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
453
|
+
input_f.close()
|
454
|
+
print(f"Closed input file: {input_f.name}") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
455
|
+
except Exception as e:
|
456
|
+
print(f"Error processing input file: {e}")
|
457
|
+
|
458
|
+
except SystemExit as e:
|
459
|
+
print(f"\nExiting application (SystemExit code: {e.code}).")
|
460
|
+
except FileNotFoundError as e:
|
461
|
+
print(f"\nError: Required file not found: {e.filename}")
|
462
|
+
parser_for_debug.print_usage()
|
463
|
+
except Exception as e:
|
464
|
+
print(f"\nAn unexpected error occurred: {e}")
|
465
|
+
import traceback
|
466
|
+
|
467
|
+
traceback.print_exc()
|
468
|
+
finally:
|
469
|
+
import os
|
470
|
+
|
471
|
+
if os.path.exists(dummy_input_filename):
|
472
|
+
try:
|
473
|
+
os.remove(dummy_input_filename)
|
474
|
+
print(f"\nRemoved dummy input file: {dummy_input_filename}")
|
475
|
+
except Exception as e:
|
476
|
+
print(f"Warning: Could not remove dummy file '{dummy_input_filename}': {e}")
|