ostruct-cli 0.4.0__py3-none-any.whl → 0.5.0__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.
- ostruct/cli/base_errors.py +183 -0
- ostruct/cli/cli.py +822 -543
- ostruct/cli/click_options.py +320 -202
- ostruct/cli/errors.py +222 -128
- ostruct/cli/exit_codes.py +18 -0
- ostruct/cli/file_info.py +30 -14
- ostruct/cli/file_list.py +4 -10
- ostruct/cli/file_utils.py +43 -35
- ostruct/cli/path_utils.py +32 -4
- ostruct/cli/security/allowed_checker.py +8 -0
- ostruct/cli/security/base.py +46 -0
- ostruct/cli/security/errors.py +83 -103
- ostruct/cli/security/security_manager.py +22 -9
- ostruct/cli/serialization.py +25 -0
- ostruct/cli/template_filters.py +5 -3
- ostruct/cli/template_rendering.py +46 -22
- ostruct/cli/template_utils.py +12 -4
- ostruct/cli/template_validation.py +26 -8
- ostruct/cli/token_utils.py +43 -0
- ostruct/cli/validators.py +109 -0
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/METADATA +60 -21
- ostruct_cli-0.5.0.dist-info/RECORD +42 -0
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.4.0.dist-info/RECORD +0 -36
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/errors.py
CHANGED
@@ -1,52 +1,16 @@
|
|
1
1
|
"""Custom error classes for CLI error handling."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Any, Dict, List, Optional
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
5
|
|
6
|
-
import
|
7
|
-
|
8
|
-
from .security.
|
6
|
+
from .base_errors import CLIError, OstructFileNotFoundError
|
7
|
+
from .exit_codes import ExitCode
|
8
|
+
from .security.base import SecurityErrorBase
|
9
|
+
from .security.errors import SecurityErrorReasons
|
9
10
|
|
10
11
|
logger = logging.getLogger(__name__)
|
11
12
|
|
12
13
|
|
13
|
-
class CLIError(click.ClickException):
|
14
|
-
"""Base class for all CLI errors."""
|
15
|
-
|
16
|
-
def __init__(self, message: str, context: Optional[Dict[str, Any]] = None):
|
17
|
-
super().__init__(message)
|
18
|
-
self.context = context or {}
|
19
|
-
self._has_been_logged = False # Use underscore for private attribute
|
20
|
-
|
21
|
-
@property
|
22
|
-
def has_been_logged(self) -> bool:
|
23
|
-
"""Whether this error has been logged."""
|
24
|
-
return self._has_been_logged
|
25
|
-
|
26
|
-
@has_been_logged.setter
|
27
|
-
def has_been_logged(self, value: bool) -> None:
|
28
|
-
"""Set whether this error has been logged."""
|
29
|
-
self._has_been_logged = value
|
30
|
-
|
31
|
-
def show(self, file: Optional[TextIO] = None) -> None:
|
32
|
-
"""Show the error message with optional context."""
|
33
|
-
if file is None:
|
34
|
-
file = cast(TextIO, click.get_text_stream("stderr"))
|
35
|
-
|
36
|
-
# Format message with context if available
|
37
|
-
if self.context:
|
38
|
-
context_str = "\n".join(
|
39
|
-
f" {k}: {v}" for k, v in self.context.items()
|
40
|
-
)
|
41
|
-
click.secho(
|
42
|
-
f"Error: {self.message}\nContext:\n{context_str}",
|
43
|
-
fg="red",
|
44
|
-
file=file,
|
45
|
-
)
|
46
|
-
else:
|
47
|
-
click.secho(f"Error: {self.message}", fg="red", file=file)
|
48
|
-
|
49
|
-
|
50
14
|
class VariableError(CLIError):
|
51
15
|
"""Base class for variable-related errors."""
|
52
16
|
|
@@ -66,7 +30,7 @@ class VariableValueError(VariableError):
|
|
66
30
|
|
67
31
|
|
68
32
|
class InvalidJSONError(CLIError):
|
69
|
-
"""
|
33
|
+
"""Error raised when JSON is invalid."""
|
70
34
|
|
71
35
|
def __init__(
|
72
36
|
self,
|
@@ -74,29 +38,36 @@ class InvalidJSONError(CLIError):
|
|
74
38
|
source: Optional[str] = None,
|
75
39
|
context: Optional[Dict[str, Any]] = None,
|
76
40
|
):
|
41
|
+
"""Initialize invalid JSON error.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
message: Error message
|
45
|
+
source: Source of invalid JSON
|
46
|
+
context: Additional context for the error
|
47
|
+
"""
|
77
48
|
context = context or {}
|
78
49
|
if source:
|
79
50
|
context["source"] = source
|
80
|
-
super().__init__(
|
51
|
+
super().__init__(
|
52
|
+
message,
|
53
|
+
exit_code=ExitCode.DATA_ERROR,
|
54
|
+
context=context,
|
55
|
+
)
|
81
56
|
|
82
57
|
|
83
58
|
class PathError(CLIError):
|
84
59
|
"""Base class for path-related errors."""
|
85
60
|
|
86
61
|
def __init__(
|
87
|
-
self,
|
62
|
+
self,
|
63
|
+
message: str,
|
64
|
+
path: str,
|
65
|
+
context: Optional[Dict[str, Any]] = None,
|
66
|
+
exit_code: int = ExitCode.FILE_ERROR,
|
88
67
|
):
|
89
68
|
context = context or {}
|
90
69
|
context["path"] = path
|
91
|
-
super().__init__(message, context)
|
92
|
-
|
93
|
-
|
94
|
-
class FileNotFoundError(PathError):
|
95
|
-
"""Raised when a specified file does not exist."""
|
96
|
-
|
97
|
-
def __init__(self, path: str, context: Optional[Dict[str, Any]] = None):
|
98
|
-
# Use path directly as the message without prepending "File not found: "
|
99
|
-
super().__init__(path, path, context)
|
70
|
+
super().__init__(message, context=context, exit_code=exit_code)
|
100
71
|
|
101
72
|
|
102
73
|
class FileReadError(PathError):
|
@@ -116,83 +87,142 @@ class DirectoryNotFoundError(PathError):
|
|
116
87
|
"""Raised when a specified directory does not exist."""
|
117
88
|
|
118
89
|
def __init__(self, path: str, context: Optional[Dict[str, Any]] = None):
|
119
|
-
|
120
|
-
|
121
|
-
|
90
|
+
context = context or {}
|
91
|
+
context.update(
|
92
|
+
{
|
93
|
+
"details": "The specified directory does not exist or cannot be accessed",
|
94
|
+
"troubleshooting": [
|
95
|
+
"Check if the directory exists",
|
96
|
+
"Verify the path spelling is correct",
|
97
|
+
"Check directory permissions",
|
98
|
+
"Ensure parent directories exist",
|
99
|
+
"Use --allowed-dir to specify additional allowed directories",
|
100
|
+
],
|
101
|
+
}
|
102
|
+
)
|
103
|
+
super().__init__(
|
104
|
+
f"Directory not found: {path}", path=path, context=context
|
105
|
+
)
|
122
106
|
|
123
|
-
class PathSecurityError(CLIError, SecurityPathSecurityError):
|
124
|
-
"""CLI wrapper for security package's PathSecurityError.
|
125
107
|
|
126
|
-
|
127
|
-
|
128
|
-
"""
|
108
|
+
class PathSecurityError(SecurityErrorBase):
|
109
|
+
"""Error raised when a path violates security constraints."""
|
129
110
|
|
130
111
|
def __init__(
|
131
112
|
self,
|
132
113
|
message: str,
|
133
114
|
path: Optional[str] = None,
|
134
|
-
context: Optional[Dict[str, Any]] = None,
|
135
115
|
error_logged: bool = False,
|
136
|
-
|
137
|
-
|
116
|
+
wrapped: bool = False,
|
117
|
+
context: Optional[Dict[str, Any]] = None,
|
118
|
+
) -> None:
|
119
|
+
"""Initialize error.
|
138
120
|
|
139
121
|
Args:
|
140
|
-
message:
|
141
|
-
path:
|
142
|
-
|
143
|
-
|
122
|
+
message: Error message
|
123
|
+
path: Path that caused the error
|
124
|
+
error_logged: Whether error has been logged
|
125
|
+
wrapped: Whether this is a wrapped error
|
126
|
+
context: Additional error context
|
144
127
|
"""
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
message,
|
160
|
-
path,
|
161
|
-
self.context,
|
162
|
-
error_logged,
|
163
|
-
)
|
128
|
+
context = context or {}
|
129
|
+
if path is not None:
|
130
|
+
context["path"] = path
|
131
|
+
context.setdefault(
|
132
|
+
"details", "The specified path violates security constraints"
|
133
|
+
)
|
134
|
+
context.setdefault(
|
135
|
+
"troubleshooting",
|
136
|
+
[
|
137
|
+
"Check if the path is within allowed directories",
|
138
|
+
"Use --allowed-dir to specify additional allowed directories",
|
139
|
+
"Verify path permissions",
|
140
|
+
],
|
141
|
+
)
|
164
142
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
super().show(file)
|
143
|
+
super().__init__(message, context=context)
|
144
|
+
self._wrapped = wrapped
|
145
|
+
self._error_logged = error_logged
|
169
146
|
|
170
147
|
@property
|
171
|
-
def
|
148
|
+
def error_logged(self) -> bool:
|
172
149
|
"""Whether this error has been logged."""
|
173
|
-
return self.
|
174
|
-
|
175
|
-
@has_been_logged.setter
|
176
|
-
def has_been_logged(self, value: bool) -> None:
|
177
|
-
"""Set whether this error has been logged."""
|
178
|
-
self._has_been_logged = value
|
179
|
-
super().has_been_logged = value # type: ignore[misc]
|
150
|
+
return self._error_logged
|
180
151
|
|
181
152
|
@property
|
182
|
-
def
|
183
|
-
"""Whether this
|
184
|
-
return self.
|
153
|
+
def wrapped(self) -> bool:
|
154
|
+
"""Whether this is a wrapped error."""
|
155
|
+
return self._wrapped
|
156
|
+
|
157
|
+
@classmethod
|
158
|
+
def from_expanded_paths(
|
159
|
+
cls,
|
160
|
+
original_path: str,
|
161
|
+
expanded_path: str,
|
162
|
+
base_dir: str,
|
163
|
+
allowed_dirs: List[str],
|
164
|
+
error_logged: bool = False,
|
165
|
+
) -> "PathSecurityError":
|
166
|
+
"""Create error with expanded path information.
|
185
167
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
168
|
+
Args:
|
169
|
+
original_path: Original path provided
|
170
|
+
expanded_path: Expanded absolute path
|
171
|
+
base_dir: Base directory
|
172
|
+
allowed_dirs: List of allowed directories
|
173
|
+
error_logged: Whether error has been logged
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
PathSecurityError instance
|
177
|
+
"""
|
178
|
+
context = {
|
179
|
+
"original_path": original_path,
|
180
|
+
"expanded_path": expanded_path,
|
181
|
+
"base_dir": base_dir,
|
182
|
+
"allowed_dirs": allowed_dirs,
|
183
|
+
"reason": SecurityErrorReasons.PATH_OUTSIDE_ALLOWED,
|
184
|
+
"details": "The path resolves to a location outside the allowed directories",
|
185
|
+
"troubleshooting": [
|
186
|
+
f"Ensure path is within base directory: {base_dir}",
|
187
|
+
"Use --allowed-dir to specify additional allowed directories",
|
188
|
+
f"Current allowed directories: {', '.join(allowed_dirs)}",
|
189
|
+
],
|
190
|
+
}
|
191
|
+
|
192
|
+
return cls(
|
193
|
+
f"Access denied: {original_path!r} resolves to {expanded_path!r} which is "
|
194
|
+
f"outside base directory {base_dir!r}",
|
195
|
+
path=original_path,
|
196
|
+
error_logged=error_logged,
|
197
|
+
context=context,
|
198
|
+
)
|
190
199
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
200
|
+
@classmethod
|
201
|
+
def wrap_error(cls, msg: str, original: Exception) -> "PathSecurityError":
|
202
|
+
"""Wrap an existing error with additional context.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
msg: New error message
|
206
|
+
original: Original error to wrap
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
New PathSecurityError instance
|
210
|
+
"""
|
211
|
+
context = {
|
212
|
+
"wrapped_error": type(original).__name__,
|
213
|
+
"original_message": str(original),
|
214
|
+
}
|
215
|
+
|
216
|
+
if hasattr(original, "context"):
|
217
|
+
context.update(original.context)
|
218
|
+
|
219
|
+
return cls(
|
220
|
+
f"{msg}: {str(original)}",
|
221
|
+
path=getattr(original, "path", None),
|
222
|
+
error_logged=getattr(original, "error_logged", False),
|
223
|
+
wrapped=True,
|
224
|
+
context=context,
|
225
|
+
)
|
196
226
|
|
197
227
|
|
198
228
|
class TaskTemplateError(CLIError):
|
@@ -210,7 +240,22 @@ class TaskTemplateSyntaxError(TaskTemplateError):
|
|
210
240
|
class TaskTemplateVariableError(TaskTemplateError):
|
211
241
|
"""Raised when a task template uses undefined variables."""
|
212
242
|
|
213
|
-
|
243
|
+
def __init__(
|
244
|
+
self,
|
245
|
+
message: str,
|
246
|
+
context: Optional[Dict[str, Any]] = None,
|
247
|
+
) -> None:
|
248
|
+
"""Initialize error.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
message: Error message
|
252
|
+
context: Additional error context
|
253
|
+
"""
|
254
|
+
super().__init__(
|
255
|
+
message,
|
256
|
+
context=context,
|
257
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
258
|
+
)
|
214
259
|
|
215
260
|
|
216
261
|
class TemplateValidationError(TaskTemplateError):
|
@@ -232,18 +277,49 @@ class SchemaError(CLIError):
|
|
232
277
|
|
233
278
|
|
234
279
|
class SchemaFileError(CLIError):
|
235
|
-
"""
|
280
|
+
"""Error raised when schema file cannot be read."""
|
236
281
|
|
237
282
|
def __init__(
|
238
283
|
self,
|
239
284
|
message: str,
|
240
285
|
schema_path: Optional[str] = None,
|
241
286
|
context: Optional[Dict[str, Any]] = None,
|
242
|
-
):
|
287
|
+
) -> None:
|
288
|
+
"""Initialize schema file error.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
message: Error message
|
292
|
+
schema_path: Path to schema file
|
293
|
+
context: Additional context for the error
|
294
|
+
"""
|
243
295
|
context = context or {}
|
244
|
-
if schema_path:
|
296
|
+
if schema_path and "source" not in context:
|
245
297
|
context["schema_path"] = schema_path
|
246
|
-
|
298
|
+
context["source"] = schema_path # Use new standard field
|
299
|
+
context.setdefault(
|
300
|
+
"details",
|
301
|
+
"The schema file could not be read or contains errors",
|
302
|
+
)
|
303
|
+
context.setdefault(
|
304
|
+
"troubleshooting",
|
305
|
+
[
|
306
|
+
"Verify the schema file exists",
|
307
|
+
"Check if the schema file contains valid JSON",
|
308
|
+
"Ensure the schema follows the correct format",
|
309
|
+
"Check file permissions",
|
310
|
+
],
|
311
|
+
)
|
312
|
+
|
313
|
+
super().__init__(
|
314
|
+
message,
|
315
|
+
context=context,
|
316
|
+
exit_code=ExitCode.SCHEMA_ERROR,
|
317
|
+
)
|
318
|
+
|
319
|
+
@property
|
320
|
+
def schema_path(self) -> Optional[str]:
|
321
|
+
"""Get the schema path."""
|
322
|
+
return self.context.get("schema_path")
|
247
323
|
|
248
324
|
|
249
325
|
class SchemaValidationError(CLIError):
|
@@ -258,7 +334,23 @@ class SchemaValidationError(CLIError):
|
|
258
334
|
context = context or {}
|
259
335
|
if schema_path:
|
260
336
|
context["schema_path"] = schema_path
|
261
|
-
|
337
|
+
context["source"] = schema_path
|
338
|
+
context.setdefault("details", "The schema validation failed")
|
339
|
+
context.setdefault(
|
340
|
+
"troubleshooting",
|
341
|
+
[
|
342
|
+
"Check if the schema follows JSON Schema specification",
|
343
|
+
"Verify all required fields are present",
|
344
|
+
"Ensure field types are correctly specified",
|
345
|
+
"Check for any syntax errors in the schema",
|
346
|
+
],
|
347
|
+
)
|
348
|
+
|
349
|
+
super().__init__(
|
350
|
+
message,
|
351
|
+
context=context,
|
352
|
+
exit_code=ExitCode.SCHEMA_ERROR,
|
353
|
+
)
|
262
354
|
|
263
355
|
|
264
356
|
class ModelCreationError(CLIError):
|
@@ -307,12 +399,6 @@ class ModelNotSupportedError(CLIError):
|
|
307
399
|
pass
|
308
400
|
|
309
401
|
|
310
|
-
class ModelVersionError(CLIError):
|
311
|
-
"""Exception raised when a model version is not supported."""
|
312
|
-
|
313
|
-
pass
|
314
|
-
|
315
|
-
|
316
402
|
class StreamInterruptedError(CLIError):
|
317
403
|
"""Exception raised when a stream is interrupted."""
|
318
404
|
|
@@ -350,18 +436,27 @@ class InvalidResponseFormatError(CLIError):
|
|
350
436
|
|
351
437
|
|
352
438
|
class OpenAIClientError(CLIError):
|
353
|
-
"""Exception raised when there's an error with the OpenAI client.
|
439
|
+
"""Exception raised when there's an error with the OpenAI client.
|
354
440
|
|
355
|
-
|
441
|
+
This is a wrapper around openai_structured's OpenAIClientError to maintain
|
442
|
+
compatibility with our CLI error handling.
|
443
|
+
"""
|
444
|
+
|
445
|
+
def __init__(
|
446
|
+
self,
|
447
|
+
message: str,
|
448
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
449
|
+
context: Optional[Dict[str, Any]] = None,
|
450
|
+
):
|
451
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
356
452
|
|
357
453
|
|
358
454
|
# Export public API
|
359
455
|
__all__ = [
|
360
|
-
"CLIError",
|
361
456
|
"VariableError",
|
362
457
|
"PathError",
|
363
458
|
"PathSecurityError",
|
364
|
-
"
|
459
|
+
"OstructFileNotFoundError",
|
365
460
|
"FileReadError",
|
366
461
|
"DirectoryNotFoundError",
|
367
462
|
"SchemaValidationError",
|
@@ -369,7 +464,6 @@ __all__ = [
|
|
369
464
|
"InvalidJSONError",
|
370
465
|
"ModelCreationError",
|
371
466
|
"ModelNotSupportedError",
|
372
|
-
"ModelVersionError",
|
373
467
|
"StreamInterruptedError",
|
374
468
|
"StreamBufferError",
|
375
469
|
"StreamParseError",
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""Exit codes for CLI operations."""
|
2
|
+
|
3
|
+
from enum import IntEnum
|
4
|
+
|
5
|
+
|
6
|
+
class ExitCode(IntEnum):
|
7
|
+
"""Exit codes for CLI operations."""
|
8
|
+
|
9
|
+
SUCCESS = 0
|
10
|
+
INTERNAL_ERROR = 1
|
11
|
+
USAGE_ERROR = 2
|
12
|
+
DATA_ERROR = 3
|
13
|
+
VALIDATION_ERROR = 4
|
14
|
+
API_ERROR = 5
|
15
|
+
SCHEMA_ERROR = 6
|
16
|
+
UNKNOWN_ERROR = 7
|
17
|
+
SECURITY_ERROR = 8
|
18
|
+
FILE_ERROR = 9
|
ostruct/cli/file_info.py
CHANGED
@@ -3,9 +3,10 @@
|
|
3
3
|
import hashlib
|
4
4
|
import logging
|
5
5
|
import os
|
6
|
+
from pathlib import Path
|
6
7
|
from typing import Any, Optional
|
7
8
|
|
8
|
-
from .errors import
|
9
|
+
from .errors import FileReadError, OstructFileNotFoundError, PathSecurityError
|
9
10
|
from .security import SecurityManager
|
10
11
|
|
11
12
|
logger = logging.getLogger(__name__)
|
@@ -73,7 +74,7 @@ class FileInfo:
|
|
73
74
|
# Check if it's a regular file (not a directory, device, etc.)
|
74
75
|
if not resolved_path.is_file():
|
75
76
|
logger.debug("Not a regular file: %s", resolved_path)
|
76
|
-
raise
|
77
|
+
raise OstructFileNotFoundError(
|
77
78
|
f"Not a regular file: {os.path.basename(str(path))}"
|
78
79
|
)
|
79
80
|
|
@@ -98,10 +99,10 @@ class FileInfo:
|
|
98
99
|
)
|
99
100
|
raise
|
100
101
|
|
101
|
-
except
|
102
|
+
except OstructFileNotFoundError as e:
|
102
103
|
# Re-raise with standardized message format
|
103
104
|
logger.debug("File not found error: %s", e)
|
104
|
-
raise
|
105
|
+
raise OstructFileNotFoundError(
|
105
106
|
f"File not found: {os.path.basename(str(path))}"
|
106
107
|
) from e
|
107
108
|
|
@@ -126,16 +127,31 @@ class FileInfo:
|
|
126
127
|
|
127
128
|
@property
|
128
129
|
def path(self) -> str:
|
129
|
-
"""Get the relative
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
130
|
+
"""Get the path relative to security manager's base directory.
|
131
|
+
|
132
|
+
Returns a path relative to the security manager's base directory.
|
133
|
+
This ensures consistent path handling across the entire codebase.
|
134
|
+
|
135
|
+
Example:
|
136
|
+
security_manager = SecurityManager(base_dir="/base")
|
137
|
+
file_info = FileInfo("/base/file.txt", security_manager)
|
138
|
+
print(file_info.path) # Outputs: "file.txt"
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
str: Path relative to security manager's base directory
|
142
|
+
|
143
|
+
Raises:
|
144
|
+
ValueError: If the path is not within the base directory
|
145
|
+
"""
|
146
|
+
try:
|
147
|
+
abs_path = Path(self.abs_path)
|
148
|
+
base_dir = Path(self.__security_manager.base_dir)
|
149
|
+
return str(abs_path.relative_to(base_dir))
|
150
|
+
except ValueError as e:
|
151
|
+
logger.error("Error making path relative: %s", e)
|
152
|
+
raise ValueError(
|
153
|
+
f"Path {abs_path} must be within base directory {base_dir}"
|
154
|
+
)
|
139
155
|
|
140
156
|
@path.setter
|
141
157
|
def path(self, value: str) -> None:
|
ostruct/cli/file_list.py
CHANGED
@@ -69,16 +69,13 @@ class FileInfoList(List[FileInfo]):
|
|
69
69
|
|
70
70
|
Returns:
|
71
71
|
Union[str, List[str]]: For a single file from file mapping, returns its content as a string.
|
72
|
-
For multiple files
|
73
|
-
|
74
|
-
Raises:
|
75
|
-
ValueError: If the list is empty
|
72
|
+
For multiple files, directory mapping, or empty list, returns a list of contents.
|
76
73
|
"""
|
77
74
|
# Take snapshot under lock
|
78
75
|
with self._lock:
|
79
76
|
if not self:
|
80
77
|
logger.debug("FileInfoList.content called but list is empty")
|
81
|
-
|
78
|
+
return []
|
82
79
|
|
83
80
|
# Make a copy of the files we need to access
|
84
81
|
if len(self) == 1 and not self._from_dir:
|
@@ -112,15 +109,12 @@ class FileInfoList(List[FileInfo]):
|
|
112
109
|
|
113
110
|
Returns:
|
114
111
|
Union[str, List[str]]: For a single file from file mapping, returns its path as a string.
|
115
|
-
For multiple files
|
116
|
-
|
117
|
-
Raises:
|
118
|
-
ValueError: If the list is empty
|
112
|
+
For multiple files, directory mapping, or empty list, returns a list of paths.
|
119
113
|
"""
|
120
114
|
# First get a snapshot of the list state under the lock
|
121
115
|
with self._lock:
|
122
116
|
if not self:
|
123
|
-
|
117
|
+
return []
|
124
118
|
if len(self) == 1 and not self._from_dir:
|
125
119
|
file_info = self[0]
|
126
120
|
is_single = True
|