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.
Files changed (83) hide show
  1. kash/actions/core/format_markdown_template.py +2 -5
  2. kash/actions/core/markdownify.py +2 -4
  3. kash/actions/core/readability.py +2 -4
  4. kash/actions/core/render_as_html.py +30 -11
  5. kash/actions/core/show_webpage.py +6 -11
  6. kash/actions/core/strip_html.py +4 -8
  7. kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
  8. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  9. kash/commands/base/basic_file_commands.py +21 -3
  10. kash/commands/base/files_command.py +29 -10
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +2 -3
  14. kash/config/colors.py +2 -2
  15. kash/config/env_settings.py +2 -42
  16. kash/config/logger.py +30 -25
  17. kash/config/logger_basic.py +6 -6
  18. kash/config/settings.py +23 -7
  19. kash/config/setup.py +33 -5
  20. kash/config/text_styles.py +25 -22
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +10 -25
  26. kash/exec/action_exec.py +43 -23
  27. kash/exec/llm_transforms.py +6 -3
  28. kash/exec/preconditions.py +10 -12
  29. kash/exec/resolve_args.py +4 -0
  30. kash/exec/runtime_settings.py +134 -0
  31. kash/exec/shell_callable_action.py +5 -3
  32. kash/file_storage/file_store.py +37 -38
  33. kash/file_storage/item_file_format.py +6 -3
  34. kash/file_storage/store_filenames.py +6 -3
  35. kash/help/function_param_info.py +1 -1
  36. kash/llm_utils/init_litellm.py +16 -0
  37. kash/llm_utils/llm_api_keys.py +6 -2
  38. kash/llm_utils/llm_completion.py +11 -4
  39. kash/local_server/local_server_routes.py +1 -7
  40. kash/mcp/mcp_cli.py +3 -2
  41. kash/mcp/mcp_server_routes.py +11 -12
  42. kash/media_base/transcription_deepgram.py +15 -2
  43. kash/model/__init__.py +1 -1
  44. kash/model/actions_model.py +6 -54
  45. kash/model/exec_model.py +79 -0
  46. kash/model/items_model.py +102 -35
  47. kash/model/operations_model.py +38 -15
  48. kash/model/paths_model.py +2 -0
  49. kash/shell/output/shell_output.py +10 -8
  50. kash/shell/shell_main.py +2 -2
  51. kash/shell/utils/exception_printing.py +2 -2
  52. kash/shell/utils/shell_function_wrapper.py +15 -15
  53. kash/text_handling/doc_normalization.py +16 -8
  54. kash/text_handling/markdown_render.py +1 -0
  55. kash/text_handling/markdown_utils.py +105 -2
  56. kash/utils/common/format_utils.py +2 -8
  57. kash/utils/common/function_inspect.py +360 -110
  58. kash/utils/common/inflection.py +22 -0
  59. kash/utils/common/task_stack.py +4 -15
  60. kash/utils/errors.py +14 -9
  61. kash/utils/file_utils/file_ext.py +4 -0
  62. kash/utils/file_utils/file_formats_model.py +32 -1
  63. kash/utils/file_utils/file_sort_filter.py +10 -3
  64. kash/web_gen/__init__.py +0 -4
  65. kash/web_gen/simple_webpage.py +52 -0
  66. kash/web_gen/tabbed_webpage.py +23 -16
  67. kash/web_gen/template_render.py +37 -2
  68. kash/web_gen/templates/base_styles.css.jinja +84 -59
  69. kash/web_gen/templates/base_webpage.html.jinja +85 -67
  70. kash/web_gen/templates/item_view.html.jinja +47 -37
  71. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  72. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
  73. kash/workspaces/__init__.py +12 -3
  74. kash/workspaces/workspace_dirs.py +58 -0
  75. kash/workspaces/workspace_importing.py +1 -1
  76. kash/workspaces/workspaces.py +26 -90
  77. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
  78. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
  79. kash/shell/utils/argparse_utils.py +0 -20
  80. kash/utils/lang_utils/inflection.py +0 -18
  81. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
  82. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
  83. {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
- type: type | None
15
- default: Any
16
- position: int | None
17
- is_varargs: bool
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
- @property
20
- def is_positional(self) -> bool:
21
- """Is this a purely positional parameter, with no default value?"""
22
- return self.position is not None and not self.has_default
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
- def inspect_function_params(func: Callable[..., Any], unwrap: bool = True) -> list[FuncParam]:
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
- Get names and types of parameters on a Python function. Just a convenience wrapper
33
- around `inspect.signature` to give parameter names, types, and default values.
34
-
35
- It also parses `Optional` types to return the underlying type and unwraps
36
- decorated functions to get the underlying parameters. Returns a list of
37
- `FuncParam` values.
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
- unwrapped = inspect.unwrap(func) if unwrap else func
46
- signature = inspect.signature(unwrapped)
47
- params: list[FuncParam] = []
70
+ if annotation is Parameter.empty or annotation is Any:
71
+ return (None, None, False)
48
72
 
49
- for i, param in enumerate(signature.parameters.values()):
50
- is_positional = param.kind in (
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
- return params
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 _extract_simple_type(annotation: Any) -> type | None:
119
+ def inspect_function_params(func: Callable[..., Any], unwrap: bool = True) -> list[FuncParam]:
78
120
  """
79
- Extract a single Type from an annotation that is an explicit simple type (like `str` or
80
- an enum) or a simple Union (such as `str` from `Optional[str]`). Return None if it's not
81
- clear.
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
- if isinstance(annotation, type):
84
- return annotation
126
+ unwrapped_func = inspect.unwrap(func) if unwrap else func
127
+ signature = inspect.signature(unwrapped_func)
85
128
 
86
- # Handle pipe syntax (str | None) from Python 3.10+
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
- origin = get_origin(annotation)
94
- if origin is Union: # pyright: ignore
95
- args = get_args(annotation)
96
- non_none_args = [arg for arg in args if arg is not type(None)]
97
- if len(non_none_args) == 1 and isinstance(non_none_args[0], type):
98
- return non_none_args[0]
99
- elif origin and isinstance(origin, type):
100
- # Cast origin to type to satisfy the type checker
101
- return cast(type, origin) # pyright: ignore
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 None
163
+ return param_infos
104
164
 
105
165
 
106
166
  ## Tests
107
167
 
108
168
 
109
- def test_inspect_function_params():
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): # pyright: ignore[reportUnusedParameter]
128
- pass
188
+ def func5(x: int, y: int = 3, *, z: int = 4, **kwargs):
189
+ return [x, y, z, kwargs]
129
190
 
130
- params0 = inspect_function_params(func0)
131
- params1 = inspect_function_params(func1)
132
- params2 = inspect_function_params(func2)
133
- params3 = inspect_function_params(func3)
134
- params4 = inspect_function_params(func4)
135
- params5 = inspect_function_params(func5)
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
- print("\ninspect:")
138
- print(repr(params0))
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(name="arg1", type=str, default=NO_DEFAULT, position=1, is_varargs=False),
154
- FuncParam(name="arg2", type=str, default=NO_DEFAULT, position=2, is_varargs=False),
155
- FuncParam(name="arg3", type=int, default=NO_DEFAULT, position=3, is_varargs=False),
156
- FuncParam(name="option_one", type=bool, default=False, position=4, is_varargs=False),
157
- FuncParam(name="option_two", type=str, default=None, position=5, is_varargs=False),
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(name="paths", type=str, default=NO_DEFAULT, position=1, is_varargs=True),
162
- FuncParam(name="summary", type=bool, default=False, position=None, is_varargs=False),
163
- FuncParam(name="iso_time", type=bool, default=False, position=None, is_varargs=False),
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(name="arg1", type=str, default=NO_DEFAULT, position=1, is_varargs=False),
168
- FuncParam(name="keywords", type=None, default=NO_DEFAULT, position=None, is_varargs=True),
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(name="x", type=int, default=NO_DEFAULT, position=1, is_varargs=False),
175
- FuncParam(name="y", type=int, default=3, position=2, is_varargs=False),
176
- FuncParam(name="z", type=int, default=4, position=None, is_varargs=False),
177
- FuncParam(name="kwargs", type=None, default=NO_DEFAULT, position=None, is_varargs=True),
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)
@@ -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
- prefix = f"{self.prefix_str()} {EMOJI_BREADCRUMB_SEP} "
97
- sep = f"\n{prefix}"
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._print()
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.warning(
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
- def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
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 litellm
166
+ import openai
159
167
 
160
- exceptions.append(litellm.exceptions.APIError)
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 NONFATAL_EXCEPTIONS:
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