kash-shell 0.3.10__py3-none-any.whl → 0.3.12__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.
- kash/actions/core/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +2 -4
- kash/actions/core/readability.py +2 -4
- kash/actions/core/render_as_html.py +30 -11
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +4 -8
- kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
- kash/commands/base/basic_file_commands.py +21 -3
- kash/commands/base/files_command.py +29 -10
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +2 -3
- kash/config/colors.py +2 -2
- kash/config/env_settings.py +2 -42
- kash/config/logger.py +30 -25
- kash/config/logger_basic.py +6 -6
- kash/config/settings.py +23 -7
- kash/config/setup.py +33 -5
- kash/config/text_styles.py +25 -22
- kash/embeddings/cosine.py +12 -4
- kash/embeddings/embeddings.py +16 -6
- kash/embeddings/text_similarity.py +10 -4
- kash/exec/__init__.py +3 -0
- kash/exec/action_decorators.py +10 -25
- kash/exec/action_exec.py +43 -23
- kash/exec/llm_transforms.py +6 -3
- kash/exec/preconditions.py +10 -12
- kash/exec/resolve_args.py +4 -0
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +37 -38
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +6 -3
- kash/help/function_param_info.py +1 -1
- kash/llm_utils/init_litellm.py +16 -0
- kash/llm_utils/llm_api_keys.py +6 -2
- kash/llm_utils/llm_completion.py +11 -4
- kash/local_server/local_server_routes.py +1 -7
- kash/mcp/mcp_cli.py +3 -2
- kash/mcp/mcp_server_routes.py +11 -12
- kash/media_base/transcription_deepgram.py +15 -2
- kash/model/__init__.py +1 -1
- kash/model/actions_model.py +6 -54
- kash/model/exec_model.py +79 -0
- kash/model/items_model.py +102 -35
- kash/model/operations_model.py +38 -15
- kash/model/paths_model.py +2 -0
- kash/shell/output/shell_output.py +10 -8
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +2 -2
- kash/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/doc_normalization.py +16 -8
- kash/text_handling/markdown_render.py +1 -0
- kash/text_handling/markdown_utils.py +105 -2
- kash/utils/common/format_utils.py +2 -8
- kash/utils/common/function_inspect.py +360 -110
- kash/utils/common/inflection.py +22 -0
- kash/utils/common/task_stack.py +4 -15
- kash/utils/errors.py +14 -9
- kash/utils/file_utils/file_ext.py +4 -0
- kash/utils/file_utils/file_formats_model.py +32 -1
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/web_gen/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +23 -16
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +84 -59
- kash/web_gen/templates/base_webpage.html.jinja +85 -67
- kash/web_gen/templates/item_view.html.jinja +47 -37
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
- kash/workspaces/__init__.py +12 -3
- kash/workspaces/workspace_dirs.py +58 -0
- kash/workspaces/workspace_importing.py +1 -1
- kash/workspaces/workspaces.py +26 -90
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,111 +2,172 @@ import inspect
|
|
|
2
2
|
import types
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
5
6
|
from inspect import Parameter
|
|
6
|
-
from typing import Any, Union, cast, get_args, get_origin # pyright: ignore
|
|
7
|
+
from typing import Any, Union, cast, get_args, get_origin # pyright: ignore[reportDeprecated]
|
|
7
8
|
|
|
8
|
-
NO_DEFAULT = Parameter.empty
|
|
9
|
+
NO_DEFAULT = Parameter.empty # Alias for clarity
|
|
10
|
+
|
|
11
|
+
ParameterKind = Parameter.POSITIONAL_ONLY.__class__
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@dataclass(frozen=True)
|
|
12
15
|
class FuncParam:
|
|
16
|
+
"""
|
|
17
|
+
Hold structured information about a single function parameter.
|
|
18
|
+
"""
|
|
19
|
+
|
|
13
20
|
name: str
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
kind: ParameterKind # e.g., POSITIONAL_OR_KEYWORD
|
|
22
|
+
annotation: Any # The raw type annotation (can be Parameter.empty)
|
|
23
|
+
default: Any # The raw default value (can be Parameter.empty)
|
|
24
|
+
position: int | None # Position index for positional args, None for keyword-only
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
# Resolved type information
|
|
27
|
+
# This is the concrete, simplified main type (e.g., str from str | None, list from list[int])
|
|
28
|
+
effective_type: type | None
|
|
29
|
+
inner_type: type | None # For collections, the type of elements (e.g., str from list[str])
|
|
30
|
+
is_explicitly_optional: bool # True if original annotation was Optional[X] or X | None
|
|
23
31
|
|
|
24
32
|
@property
|
|
25
33
|
def has_default(self) -> bool:
|
|
26
34
|
"""Does this parameter have a default value?"""
|
|
27
35
|
return self.default != NO_DEFAULT
|
|
28
36
|
|
|
37
|
+
@property
|
|
38
|
+
def is_pure_positional(self) -> bool:
|
|
39
|
+
"""Is this a plain or varargs positional parameter, with no default value?"""
|
|
40
|
+
return self.position is not None and not self.has_default
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_positional_only(self) -> bool:
|
|
44
|
+
"""Is a true positional parameter, with no default value."""
|
|
45
|
+
return self.kind == Parameter.POSITIONAL_ONLY
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
@property
|
|
48
|
+
def is_positional_or_keyword(self) -> bool:
|
|
49
|
+
return self.kind == Parameter.POSITIONAL_OR_KEYWORD
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_varargs(self) -> bool:
|
|
53
|
+
return self.kind == Parameter.VAR_POSITIONAL or self.kind == Parameter.VAR_KEYWORD
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_keyword_only(self) -> bool:
|
|
57
|
+
return self.kind == Parameter.KEYWORD_ONLY
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_type_details(annotation: Any) -> tuple[type | None, type | None, bool]:
|
|
31
61
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Note that technically, parameters are of 5 types, positional-only,
|
|
40
|
-
positional-or-keyword, keyword-only, varargs, or varargs-keyword, which is what
|
|
41
|
-
`inspect` tells you, but in practice you typically just want to know
|
|
42
|
-
`FuncParam.is_positional()`, i.e. if it is a pure positional parameter (no default)
|
|
43
|
-
and not a keyword (with a default).
|
|
62
|
+
Resolves an annotation into (effective_type, inner_type, is_explicitly_optional).
|
|
63
|
+
|
|
64
|
+
- effective_type: The main type (e.g., str from Optional[str], list from list[int]).
|
|
65
|
+
- inner_type: The type of elements if effective_type is a collection
|
|
66
|
+
(e.g., int from list[int]). For non-collection or unparameterized
|
|
67
|
+
collection, this is None.
|
|
68
|
+
- is_explicitly_optional: True if the annotation was X | None or Optional[X].
|
|
44
69
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
params: list[FuncParam] = []
|
|
70
|
+
if annotation is Parameter.empty or annotation is Any:
|
|
71
|
+
return (None, None, False)
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Parameter.POSITIONAL_ONLY,
|
|
52
|
-
Parameter.POSITIONAL_OR_KEYWORD,
|
|
53
|
-
Parameter.VAR_POSITIONAL,
|
|
54
|
-
)
|
|
55
|
-
is_varargs = param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD)
|
|
56
|
-
has_default = param.default != NO_DEFAULT
|
|
57
|
-
|
|
58
|
-
# Get type from type annotation or default value.
|
|
59
|
-
param_type: type | None = None
|
|
60
|
-
if param.annotation != Parameter.empty:
|
|
61
|
-
param_type = _extract_simple_type(param.annotation)
|
|
62
|
-
elif param.default is not Parameter.empty:
|
|
63
|
-
param_type = type(param.default)
|
|
64
|
-
|
|
65
|
-
func_param = FuncParam(
|
|
66
|
-
name=param.name,
|
|
67
|
-
type=param_type,
|
|
68
|
-
default=param.default if has_default else NO_DEFAULT,
|
|
69
|
-
position=i + 1 if is_positional else None,
|
|
70
|
-
is_varargs=is_varargs,
|
|
71
|
-
)
|
|
72
|
-
params.append(func_param)
|
|
73
|
+
current_annotation = annotation
|
|
74
|
+
is_optional_flag = False
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
# Unwrap Optional[T] or T | None (from Python 3.10+ UnionType)
|
|
77
|
+
origin: type | None = get_origin(current_annotation)
|
|
78
|
+
args = get_args(current_annotation)
|
|
79
|
+
|
|
80
|
+
if origin is Union or ( # pyright: ignore[reportDeprecated]
|
|
81
|
+
hasattr(types, "UnionType") and isinstance(current_annotation, types.UnionType)
|
|
82
|
+
):
|
|
83
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
84
|
+
if len(non_none_args) == 1:
|
|
85
|
+
is_optional_flag = True
|
|
86
|
+
current_annotation = non_none_args[0]
|
|
87
|
+
origin = get_origin(current_annotation) # Re-evaluate for the unwrapped type
|
|
88
|
+
args = get_args(current_annotation)
|
|
89
|
+
elif not non_none_args: # Handles Union[NoneType] or just NoneType
|
|
90
|
+
return (type(None), None, True)
|
|
91
|
+
# If multiple non_none_args (e.g., int | str), current_annotation remains the Union for now.
|
|
92
|
+
|
|
93
|
+
# Determine effective_type and inner_type from (potentially unwrapped) current_annotation
|
|
94
|
+
final_effective_type: type | None = None
|
|
95
|
+
final_inner_type: type | None = None
|
|
96
|
+
|
|
97
|
+
if isinstance(
|
|
98
|
+
current_annotation, type
|
|
99
|
+
): # Covers simple types (int, str) and resolved Union types (int | str)
|
|
100
|
+
final_effective_type = current_annotation
|
|
101
|
+
elif origin and isinstance(origin, type): # Generics like list, dict, tuple (e.g., list[str])
|
|
102
|
+
final_effective_type = cast(type, origin) # This would be `list` for `list[str]`
|
|
103
|
+
if args and _is_type_tuple(args) and args[0] is not Any:
|
|
104
|
+
# For simplicity, take the first type argument as inner_type.
|
|
105
|
+
# E.g., for list[str], inner_type is str. For dict[str, int], inner_type is str.
|
|
106
|
+
final_inner_type = args[0]
|
|
107
|
+
# A more sophisticated approach might handle all args for tuples like tuple[str, int]
|
|
108
|
+
|
|
109
|
+
return final_effective_type, final_inner_type, is_optional_flag
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_type_tuple(args: tuple[Any, ...]) -> bool:
|
|
113
|
+
"""Are all args types?"""
|
|
114
|
+
if not args:
|
|
115
|
+
return False
|
|
116
|
+
return all(isinstance(arg, type) for arg in args)
|
|
75
117
|
|
|
76
118
|
|
|
77
|
-
def
|
|
119
|
+
def inspect_function_params(func: Callable[..., Any], unwrap: bool = True) -> list[FuncParam]:
|
|
78
120
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
Inspects a Python function's signature and returns a list of `ParamInfo` objects.
|
|
122
|
+
A convenience wrapper for `inspect.signature` that provides a detailed, structured
|
|
123
|
+
representation of each parameter, making it easier to build tools like CLI argument
|
|
124
|
+
parsers. By default, it unwraps decorated functions to get to the original signature.
|
|
82
125
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
unwrapped_func = inspect.unwrap(func) if unwrap else func
|
|
127
|
+
signature = inspect.signature(unwrapped_func)
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
if hasattr(types, "UnionType") and isinstance(annotation, types.UnionType):
|
|
88
|
-
args = get_args(annotation)
|
|
89
|
-
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
90
|
-
if len(non_none_args) == 1 and isinstance(non_none_args[0], type):
|
|
91
|
-
return non_none_args[0]
|
|
129
|
+
param_infos: list[FuncParam] = []
|
|
92
130
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
for i, (param_name, param_obj) in enumerate(signature.parameters.items()):
|
|
132
|
+
effective_type, inner_type, is_optional = _resolve_type_details(param_obj.annotation)
|
|
133
|
+
|
|
134
|
+
# Fallback: if type is not resolved from annotation, try to infer from default value.
|
|
135
|
+
if (
|
|
136
|
+
effective_type is None
|
|
137
|
+
and param_obj.default is not NO_DEFAULT
|
|
138
|
+
and param_obj.default is not None
|
|
139
|
+
):
|
|
140
|
+
if not is_optional: # Avoid setting NoneType if it was Optional[SomethingElse]
|
|
141
|
+
effective_type = type(param_obj.default)
|
|
142
|
+
|
|
143
|
+
# Determine position
|
|
144
|
+
is_positional = param_obj.kind in (
|
|
145
|
+
Parameter.POSITIONAL_ONLY,
|
|
146
|
+
Parameter.POSITIONAL_OR_KEYWORD,
|
|
147
|
+
Parameter.VAR_POSITIONAL,
|
|
148
|
+
)
|
|
149
|
+
position = i + 1 if is_positional else None
|
|
150
|
+
|
|
151
|
+
info = FuncParam(
|
|
152
|
+
name=param_name,
|
|
153
|
+
kind=param_obj.kind,
|
|
154
|
+
annotation=param_obj.annotation, # Store raw annotation
|
|
155
|
+
default=param_obj.default, # Store raw default
|
|
156
|
+
position=position,
|
|
157
|
+
effective_type=effective_type,
|
|
158
|
+
inner_type=inner_type,
|
|
159
|
+
is_explicitly_optional=is_optional,
|
|
160
|
+
)
|
|
161
|
+
param_infos.append(info)
|
|
102
162
|
|
|
103
|
-
return
|
|
163
|
+
return param_infos
|
|
104
164
|
|
|
105
165
|
|
|
106
166
|
## Tests
|
|
107
167
|
|
|
108
168
|
|
|
109
|
-
def
|
|
169
|
+
def test_inspect_function_parameters_updated():
|
|
170
|
+
# Test functions from your original example
|
|
110
171
|
def func0(path: str | None = None) -> list:
|
|
111
172
|
return [path]
|
|
112
173
|
|
|
@@ -124,55 +185,244 @@ def test_inspect_function_params():
|
|
|
124
185
|
def func4() -> list:
|
|
125
186
|
return []
|
|
126
187
|
|
|
127
|
-
def func5(x: int, y: int = 3, *, z: int = 4, **kwargs):
|
|
128
|
-
|
|
188
|
+
def func5(x: int, y: int = 3, *, z: int = 4, **kwargs):
|
|
189
|
+
return [x, y, z, kwargs]
|
|
129
190
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
class MyEnum(Enum):
|
|
192
|
+
ITEM1 = "item1"
|
|
193
|
+
ITEM2 = "item2"
|
|
194
|
+
|
|
195
|
+
def func6(opt_enum: MyEnum | None = MyEnum.ITEM1):
|
|
196
|
+
return [opt_enum]
|
|
136
197
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
print()
|
|
140
|
-
print(repr(params1))
|
|
141
|
-
print()
|
|
142
|
-
print(repr(params2))
|
|
143
|
-
print()
|
|
144
|
-
print(repr(params3))
|
|
145
|
-
print()
|
|
146
|
-
print(repr(params4))
|
|
147
|
-
print()
|
|
148
|
-
print(repr(params5))
|
|
149
|
-
|
|
150
|
-
assert params0 == [FuncParam(name="path", type=str, default=None, position=1, is_varargs=False)]
|
|
198
|
+
def func7(numbers: list[int]):
|
|
199
|
+
return [numbers]
|
|
151
200
|
|
|
201
|
+
def func8(maybe_list: list[str] | None = None):
|
|
202
|
+
return [maybe_list]
|
|
203
|
+
|
|
204
|
+
params0 = inspect_function_params(func0)
|
|
205
|
+
print("\ninspect_function_parameters results:")
|
|
206
|
+
print(f"func0: {params0}")
|
|
207
|
+
assert params0 == [
|
|
208
|
+
FuncParam(
|
|
209
|
+
name="path",
|
|
210
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
211
|
+
annotation=(str | None),
|
|
212
|
+
default=None,
|
|
213
|
+
position=1,
|
|
214
|
+
effective_type=str,
|
|
215
|
+
inner_type=None,
|
|
216
|
+
is_explicitly_optional=True,
|
|
217
|
+
)
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
params1 = inspect_function_params(func1)
|
|
221
|
+
print(f"func1: {params1}")
|
|
152
222
|
assert params1 == [
|
|
153
|
-
FuncParam(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
FuncParam(
|
|
224
|
+
name="arg1",
|
|
225
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
226
|
+
annotation=str,
|
|
227
|
+
default=NO_DEFAULT,
|
|
228
|
+
position=1,
|
|
229
|
+
effective_type=str,
|
|
230
|
+
inner_type=None,
|
|
231
|
+
is_explicitly_optional=False,
|
|
232
|
+
),
|
|
233
|
+
FuncParam(
|
|
234
|
+
name="arg2",
|
|
235
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
236
|
+
annotation=str,
|
|
237
|
+
default=NO_DEFAULT,
|
|
238
|
+
position=2,
|
|
239
|
+
effective_type=str,
|
|
240
|
+
inner_type=None,
|
|
241
|
+
is_explicitly_optional=False,
|
|
242
|
+
),
|
|
243
|
+
FuncParam(
|
|
244
|
+
name="arg3",
|
|
245
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
246
|
+
annotation=int,
|
|
247
|
+
default=NO_DEFAULT,
|
|
248
|
+
position=3,
|
|
249
|
+
effective_type=int,
|
|
250
|
+
inner_type=None,
|
|
251
|
+
is_explicitly_optional=False,
|
|
252
|
+
),
|
|
253
|
+
FuncParam(
|
|
254
|
+
name="option_one",
|
|
255
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
256
|
+
annotation=bool,
|
|
257
|
+
default=False,
|
|
258
|
+
position=4,
|
|
259
|
+
effective_type=bool,
|
|
260
|
+
inner_type=None,
|
|
261
|
+
is_explicitly_optional=False,
|
|
262
|
+
), # bool default makes it not explicitly Optional from type
|
|
263
|
+
FuncParam(
|
|
264
|
+
name="option_two",
|
|
265
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
266
|
+
annotation=(str | None),
|
|
267
|
+
default=None,
|
|
268
|
+
position=5,
|
|
269
|
+
effective_type=str,
|
|
270
|
+
inner_type=None,
|
|
271
|
+
is_explicitly_optional=True,
|
|
272
|
+
),
|
|
158
273
|
]
|
|
159
274
|
|
|
275
|
+
params2 = inspect_function_params(func2)
|
|
276
|
+
print(f"func2: {params2}")
|
|
160
277
|
assert params2 == [
|
|
161
|
-
FuncParam(
|
|
162
|
-
|
|
163
|
-
|
|
278
|
+
FuncParam(
|
|
279
|
+
name="paths",
|
|
280
|
+
kind=Parameter.VAR_POSITIONAL,
|
|
281
|
+
annotation=str,
|
|
282
|
+
default=NO_DEFAULT,
|
|
283
|
+
position=1,
|
|
284
|
+
effective_type=str,
|
|
285
|
+
inner_type=None,
|
|
286
|
+
is_explicitly_optional=False,
|
|
287
|
+
), # For *args: T, effective_type is T
|
|
288
|
+
FuncParam(
|
|
289
|
+
name="summary",
|
|
290
|
+
kind=Parameter.KEYWORD_ONLY,
|
|
291
|
+
annotation=(bool | None),
|
|
292
|
+
default=False,
|
|
293
|
+
position=None,
|
|
294
|
+
effective_type=bool,
|
|
295
|
+
inner_type=None,
|
|
296
|
+
is_explicitly_optional=True,
|
|
297
|
+
),
|
|
298
|
+
FuncParam(
|
|
299
|
+
name="iso_time",
|
|
300
|
+
kind=Parameter.KEYWORD_ONLY,
|
|
301
|
+
annotation=bool,
|
|
302
|
+
default=False,
|
|
303
|
+
position=None,
|
|
304
|
+
effective_type=bool,
|
|
305
|
+
inner_type=None,
|
|
306
|
+
is_explicitly_optional=False,
|
|
307
|
+
),
|
|
164
308
|
]
|
|
165
309
|
|
|
310
|
+
params3 = inspect_function_params(func3)
|
|
311
|
+
print(f"func3: {params3}")
|
|
166
312
|
assert params3 == [
|
|
167
|
-
FuncParam(
|
|
168
|
-
|
|
313
|
+
FuncParam(
|
|
314
|
+
name="arg1",
|
|
315
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
316
|
+
annotation=str,
|
|
317
|
+
default=NO_DEFAULT,
|
|
318
|
+
position=1,
|
|
319
|
+
effective_type=str,
|
|
320
|
+
inner_type=None,
|
|
321
|
+
is_explicitly_optional=False,
|
|
322
|
+
),
|
|
323
|
+
FuncParam(
|
|
324
|
+
name="keywords",
|
|
325
|
+
kind=Parameter.VAR_KEYWORD,
|
|
326
|
+
annotation=Parameter.empty,
|
|
327
|
+
default=NO_DEFAULT,
|
|
328
|
+
position=None,
|
|
329
|
+
effective_type=None,
|
|
330
|
+
inner_type=None,
|
|
331
|
+
is_explicitly_optional=False,
|
|
332
|
+
),
|
|
169
333
|
]
|
|
170
334
|
|
|
335
|
+
params4 = inspect_function_params(func4)
|
|
336
|
+
print(f"func4: {params4}")
|
|
171
337
|
assert params4 == []
|
|
172
338
|
|
|
339
|
+
params5 = inspect_function_params(func5)
|
|
340
|
+
print(f"func5: {params5}")
|
|
173
341
|
assert params5 == [
|
|
174
|
-
FuncParam(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
342
|
+
FuncParam(
|
|
343
|
+
name="x",
|
|
344
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
345
|
+
annotation=int,
|
|
346
|
+
default=NO_DEFAULT,
|
|
347
|
+
position=1,
|
|
348
|
+
effective_type=int,
|
|
349
|
+
inner_type=None,
|
|
350
|
+
is_explicitly_optional=False,
|
|
351
|
+
),
|
|
352
|
+
FuncParam(
|
|
353
|
+
name="y",
|
|
354
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
355
|
+
annotation=int,
|
|
356
|
+
default=3,
|
|
357
|
+
position=2,
|
|
358
|
+
effective_type=int,
|
|
359
|
+
inner_type=None,
|
|
360
|
+
is_explicitly_optional=False,
|
|
361
|
+
),
|
|
362
|
+
FuncParam(
|
|
363
|
+
name="z",
|
|
364
|
+
kind=Parameter.KEYWORD_ONLY,
|
|
365
|
+
annotation=int,
|
|
366
|
+
default=4,
|
|
367
|
+
position=None,
|
|
368
|
+
effective_type=int,
|
|
369
|
+
inner_type=None,
|
|
370
|
+
is_explicitly_optional=False,
|
|
371
|
+
),
|
|
372
|
+
FuncParam(
|
|
373
|
+
name="kwargs",
|
|
374
|
+
kind=Parameter.VAR_KEYWORD,
|
|
375
|
+
annotation=Parameter.empty,
|
|
376
|
+
default=NO_DEFAULT,
|
|
377
|
+
position=None,
|
|
378
|
+
effective_type=None,
|
|
379
|
+
inner_type=None,
|
|
380
|
+
is_explicitly_optional=False,
|
|
381
|
+
),
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
params6 = inspect_function_params(func6)
|
|
385
|
+
print(f"func6: {params6}")
|
|
386
|
+
|
|
387
|
+
assert params6 == [
|
|
388
|
+
FuncParam(
|
|
389
|
+
name="opt_enum",
|
|
390
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
391
|
+
annotation=(MyEnum | None),
|
|
392
|
+
default=MyEnum.ITEM1,
|
|
393
|
+
position=1,
|
|
394
|
+
effective_type=MyEnum,
|
|
395
|
+
inner_type=None,
|
|
396
|
+
is_explicitly_optional=True,
|
|
397
|
+
)
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
params7 = inspect_function_params(func7)
|
|
401
|
+
print(f"func7: {params7}")
|
|
402
|
+
assert params7 == [
|
|
403
|
+
FuncParam(
|
|
404
|
+
name="numbers",
|
|
405
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
406
|
+
annotation=list[int],
|
|
407
|
+
default=NO_DEFAULT,
|
|
408
|
+
position=1,
|
|
409
|
+
effective_type=list,
|
|
410
|
+
inner_type=int,
|
|
411
|
+
is_explicitly_optional=False,
|
|
412
|
+
)
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
params8 = inspect_function_params(func8)
|
|
416
|
+
print(f"func8: {params8}")
|
|
417
|
+
assert params8 == [
|
|
418
|
+
FuncParam(
|
|
419
|
+
name="maybe_list",
|
|
420
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
421
|
+
annotation=(list[str] | None),
|
|
422
|
+
default=None,
|
|
423
|
+
position=1,
|
|
424
|
+
effective_type=list,
|
|
425
|
+
inner_type=str,
|
|
426
|
+
is_explicitly_optional=True,
|
|
427
|
+
)
|
|
178
428
|
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
|
|
3
|
+
# Had been using the `inflect` package, but it takes over 1s to import.
|
|
4
|
+
# pluralizer seems simpler and fine for common English usage.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@cache
|
|
8
|
+
def _get_pluralizer():
|
|
9
|
+
from pluralizer import Pluralizer
|
|
10
|
+
|
|
11
|
+
return Pluralizer()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plural(word: str, count: int | None = None) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Pluralize or singularize a word based on the count.
|
|
17
|
+
"""
|
|
18
|
+
from chopdiff.docs import is_word
|
|
19
|
+
|
|
20
|
+
if not is_word(word):
|
|
21
|
+
return word
|
|
22
|
+
return _get_pluralizer().pluralize(word, count=count)
|
kash/utils/common/task_stack.py
CHANGED
|
@@ -3,7 +3,6 @@ from contextlib import contextmanager
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
5
|
from kash.config.text_styles import (
|
|
6
|
-
EMOJI_ACTION,
|
|
7
6
|
EMOJI_BREADCRUMB_SEP,
|
|
8
7
|
EMOJI_MSG_INDENT,
|
|
9
8
|
TASK_STACK_HEADER,
|
|
@@ -93,9 +92,8 @@ class TaskStack:
|
|
|
93
92
|
if not self.stack:
|
|
94
93
|
return ""
|
|
95
94
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return prefix + sep.join(state.full_str() for state in self.stack)
|
|
95
|
+
sep = f" {EMOJI_BREADCRUMB_SEP} "
|
|
96
|
+
return f"{EMOJI_BREADCRUMB_SEP} " + sep.join(state.full_str() for state in self.stack)
|
|
99
97
|
|
|
100
98
|
def prefix_str(self) -> str:
|
|
101
99
|
if not self.stack:
|
|
@@ -107,8 +105,7 @@ class TaskStack:
|
|
|
107
105
|
return f"TaskStack({self.full_str()})"
|
|
108
106
|
|
|
109
107
|
def log_stack(self):
|
|
110
|
-
self.
|
|
111
|
-
self._log.message(f"{EMOJI_ACTION} {TASK_STACK_HEADER}\n%s", self.full_str())
|
|
108
|
+
self._log.message(f"{TASK_STACK_HEADER} %s", self.full_str())
|
|
112
109
|
|
|
113
110
|
@contextmanager
|
|
114
111
|
def context(self, name: str, total_parts: int = 1, unit: str = ""):
|
|
@@ -123,9 +120,7 @@ class TaskStack:
|
|
|
123
120
|
except Exception as e:
|
|
124
121
|
# Log immediately where the exception occurred, but don't double-log.
|
|
125
122
|
if e not in self.exceptions_logged:
|
|
126
|
-
self._log.
|
|
127
|
-
"Exception in task context: %s: %s", type(e).__name__, e, exc_info=True
|
|
128
|
-
)
|
|
123
|
+
self._log.info("Exception in task context: %s: %s", type(e).__name__, e)
|
|
129
124
|
self.exceptions_logged.add(e)
|
|
130
125
|
self.next(last_had_error=True)
|
|
131
126
|
raise
|
|
@@ -139,12 +134,6 @@ class TaskStack:
|
|
|
139
134
|
|
|
140
135
|
return get_logger(__name__)
|
|
141
136
|
|
|
142
|
-
@property
|
|
143
|
-
def _print(self):
|
|
144
|
-
from kash.shell.output.shell_output import cprint
|
|
145
|
-
|
|
146
|
-
return cprint
|
|
147
|
-
|
|
148
137
|
|
|
149
138
|
task_stack_var: contextvars.ContextVar[TaskStack | None] = contextvars.ContextVar(
|
|
150
139
|
"task_stack", default=None
|
kash/utils/errors.py
CHANGED
|
@@ -3,6 +3,8 @@ Common hierarchy of error types. These inherit from standard errors like
|
|
|
3
3
|
ValueError and FileExistsError but are more fine-grained.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from functools import cache
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class KashRuntimeError(ValueError):
|
|
8
10
|
"""Base class for kash runtime errors."""
|
|
@@ -145,8 +147,14 @@ class ApiError(KashRuntimeError):
|
|
|
145
147
|
pass
|
|
146
148
|
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
@cache
|
|
151
|
+
def get_nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
152
|
+
"""
|
|
153
|
+
Exceptions that are not fatal and usually don't merit a full stack trace.
|
|
154
|
+
"""
|
|
149
155
|
exceptions: list[type[Exception]] = [SelfExplanatoryError, FileNotFoundError, IOError]
|
|
156
|
+
|
|
157
|
+
# Slow imports, do lazily.
|
|
150
158
|
try:
|
|
151
159
|
from xonsh.tools import XonshError
|
|
152
160
|
|
|
@@ -155,14 +163,15 @@ def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
|
155
163
|
pass
|
|
156
164
|
|
|
157
165
|
try:
|
|
158
|
-
import
|
|
166
|
+
import openai
|
|
159
167
|
|
|
160
|
-
exceptions.
|
|
168
|
+
# LiteLLM exceptions subclass openai.APIError
|
|
169
|
+
exceptions.append(openai.APIError)
|
|
161
170
|
except ImportError:
|
|
162
171
|
pass
|
|
163
172
|
|
|
164
173
|
try:
|
|
165
|
-
import yt_dlp
|
|
174
|
+
import yt_dlp.utils
|
|
166
175
|
|
|
167
176
|
exceptions.append(yt_dlp.utils.DownloadError)
|
|
168
177
|
except ImportError:
|
|
@@ -171,12 +180,8 @@ def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
|
171
180
|
return tuple(exceptions)
|
|
172
181
|
|
|
173
182
|
|
|
174
|
-
NONFATAL_EXCEPTIONS = _nonfatal_exceptions()
|
|
175
|
-
"""Exceptions that are not fatal and usually don't merit a full stack trace."""
|
|
176
|
-
|
|
177
|
-
|
|
178
183
|
def is_fatal(exception: Exception) -> bool:
|
|
179
|
-
for e in
|
|
184
|
+
for e in get_nonfatal_exceptions():
|
|
180
185
|
if isinstance(exception, e):
|
|
181
186
|
return False
|
|
182
187
|
return True
|
|
@@ -20,6 +20,7 @@ class FileExt(Enum):
|
|
|
20
20
|
diff = "diff"
|
|
21
21
|
json = "json"
|
|
22
22
|
csv = "csv"
|
|
23
|
+
xlsx = "xlsx"
|
|
23
24
|
npz = "npz"
|
|
24
25
|
log = "log"
|
|
25
26
|
py = "py"
|
|
@@ -34,6 +35,8 @@ class FileExt(Enum):
|
|
|
34
35
|
mp3 = "mp3"
|
|
35
36
|
m4a = "m4a"
|
|
36
37
|
mp4 = "mp4"
|
|
38
|
+
pptx = "pptx"
|
|
39
|
+
epub = "epub"
|
|
37
40
|
|
|
38
41
|
@property
|
|
39
42
|
def dot_ext(self) -> str:
|
|
@@ -50,6 +53,7 @@ class FileExt(Enum):
|
|
|
50
53
|
self.py,
|
|
51
54
|
self.sh,
|
|
52
55
|
self.xsh,
|
|
56
|
+
self.epub,
|
|
53
57
|
]
|
|
54
58
|
|
|
55
59
|
@property
|