indent 0.0.8__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.

Potentially problematic release.


This version of indent might be problematic. Click here for more details.

Files changed (56) hide show
  1. exponent/__init__.py +1 -0
  2. exponent/cli.py +112 -0
  3. exponent/commands/cloud_commands.py +85 -0
  4. exponent/commands/common.py +434 -0
  5. exponent/commands/config_commands.py +581 -0
  6. exponent/commands/github_app_commands.py +211 -0
  7. exponent/commands/listen_commands.py +96 -0
  8. exponent/commands/run_commands.py +208 -0
  9. exponent/commands/settings.py +56 -0
  10. exponent/commands/shell_commands.py +2840 -0
  11. exponent/commands/theme.py +246 -0
  12. exponent/commands/types.py +111 -0
  13. exponent/commands/upgrade.py +29 -0
  14. exponent/commands/utils.py +236 -0
  15. exponent/core/config.py +180 -0
  16. exponent/core/graphql/__init__.py +0 -0
  17. exponent/core/graphql/client.py +59 -0
  18. exponent/core/graphql/cloud_config_queries.py +77 -0
  19. exponent/core/graphql/get_chats_query.py +47 -0
  20. exponent/core/graphql/github_config_queries.py +56 -0
  21. exponent/core/graphql/mutations.py +75 -0
  22. exponent/core/graphql/queries.py +110 -0
  23. exponent/core/graphql/subscriptions.py +452 -0
  24. exponent/core/remote_execution/checkpoints.py +212 -0
  25. exponent/core/remote_execution/cli_rpc_types.py +214 -0
  26. exponent/core/remote_execution/client.py +545 -0
  27. exponent/core/remote_execution/code_execution.py +58 -0
  28. exponent/core/remote_execution/command_execution.py +105 -0
  29. exponent/core/remote_execution/error_info.py +45 -0
  30. exponent/core/remote_execution/exceptions.py +10 -0
  31. exponent/core/remote_execution/file_write.py +410 -0
  32. exponent/core/remote_execution/files.py +415 -0
  33. exponent/core/remote_execution/git.py +268 -0
  34. exponent/core/remote_execution/languages/python_execution.py +239 -0
  35. exponent/core/remote_execution/languages/shell_streaming.py +221 -0
  36. exponent/core/remote_execution/languages/types.py +20 -0
  37. exponent/core/remote_execution/session.py +128 -0
  38. exponent/core/remote_execution/system_context.py +54 -0
  39. exponent/core/remote_execution/tool_execution.py +289 -0
  40. exponent/core/remote_execution/truncation.py +284 -0
  41. exponent/core/remote_execution/types.py +670 -0
  42. exponent/core/remote_execution/utils.py +600 -0
  43. exponent/core/types/__init__.py +0 -0
  44. exponent/core/types/command_data.py +206 -0
  45. exponent/core/types/event_types.py +89 -0
  46. exponent/core/types/generated/__init__.py +0 -0
  47. exponent/core/types/generated/strategy_info.py +225 -0
  48. exponent/migration-docs/login.md +112 -0
  49. exponent/py.typed +4 -0
  50. exponent/utils/__init__.py +0 -0
  51. exponent/utils/colors.py +92 -0
  52. exponent/utils/version.py +289 -0
  53. indent-0.0.8.dist-info/METADATA +36 -0
  54. indent-0.0.8.dist-info/RECORD +56 -0
  55. indent-0.0.8.dist-info/WHEEL +4 -0
  56. indent-0.0.8.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,289 @@
1
+ import uuid
2
+ from collections.abc import Callable
3
+ from pathlib import Path
4
+ from time import time
5
+
6
+ from anyio import Path as AsyncPath
7
+
8
+ from exponent.core.remote_execution import files
9
+ from exponent.core.remote_execution.cli_rpc_types import (
10
+ BashToolInput,
11
+ BashToolResult,
12
+ ErrorToolResult,
13
+ GlobToolInput,
14
+ GlobToolResult,
15
+ GrepToolInput,
16
+ GrepToolResult,
17
+ ListToolInput,
18
+ ListToolResult,
19
+ ReadToolInput,
20
+ ReadToolResult,
21
+ ToolInputType,
22
+ ToolResultType,
23
+ WriteToolInput,
24
+ WriteToolResult,
25
+ )
26
+ from exponent.core.remote_execution.code_execution import (
27
+ execute_code_streaming,
28
+ )
29
+ from exponent.core.remote_execution.file_write import execute_full_file_rewrite
30
+ from exponent.core.remote_execution.truncation import truncate_tool_result
31
+ from exponent.core.remote_execution.types import (
32
+ StreamingCodeExecutionRequest,
33
+ StreamingCodeExecutionResponse,
34
+ )
35
+ from exponent.core.remote_execution.utils import (
36
+ assert_unreachable,
37
+ safe_read_file,
38
+ )
39
+
40
+ GREP_MAX_RESULTS = 100
41
+
42
+
43
+ async def execute_tool(
44
+ tool_input: ToolInputType, working_directory: str
45
+ ) -> ToolResultType:
46
+ if isinstance(tool_input, ReadToolInput):
47
+ return await execute_read_file(tool_input, working_directory)
48
+ elif isinstance(tool_input, WriteToolInput):
49
+ return await execute_write_file(tool_input, working_directory)
50
+ elif isinstance(tool_input, ListToolInput):
51
+ return await execute_list_files(tool_input, working_directory)
52
+ elif isinstance(tool_input, GlobToolInput):
53
+ return await execute_glob_files(tool_input, working_directory)
54
+ elif isinstance(tool_input, GrepToolInput):
55
+ return await execute_grep_files(tool_input, working_directory)
56
+ elif isinstance(tool_input, BashToolInput):
57
+ raise ValueError("Bash tool input should be handled by execute_bash_tool")
58
+ else:
59
+ assert_unreachable(tool_input)
60
+
61
+
62
+ def truncate_result[T: ToolResultType](tool_result: T) -> T:
63
+ return truncate_tool_result(tool_result)
64
+
65
+
66
+ async def execute_read_file( # noqa: PLR0911
67
+ tool_input: ReadToolInput, working_directory: str
68
+ ) -> ReadToolResult | ErrorToolResult:
69
+ # Validate absolute path requirement
70
+ if not tool_input.file_path.startswith("/"):
71
+ return ErrorToolResult(
72
+ error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
73
+ )
74
+
75
+ # Validate offset and limit
76
+ offset = tool_input.offset if tool_input.offset is not None else 0
77
+ limit = tool_input.limit if tool_input.limit is not None else 2000
78
+
79
+ if offset < 0:
80
+ return ErrorToolResult(
81
+ error_message=f"Offset must be non-negative, got: {offset}"
82
+ )
83
+
84
+ if limit <= 0:
85
+ return ErrorToolResult(error_message=f"Limit must be positive, got: {limit}")
86
+
87
+ file = AsyncPath(working_directory, tool_input.file_path)
88
+
89
+ try:
90
+ exists = await file.exists()
91
+ except (OSError, PermissionError) as e:
92
+ return ErrorToolResult(error_message=f"Cannot access file: {e!s}")
93
+
94
+ if not exists:
95
+ return ErrorToolResult(
96
+ error_message="File not found",
97
+ )
98
+
99
+ try:
100
+ if await file.is_dir():
101
+ return ErrorToolResult(
102
+ error_message=f"{await file.absolute()} is a directory",
103
+ )
104
+ except (OSError, PermissionError) as e:
105
+ return ErrorToolResult(error_message=f"Cannot check file type: {e!s}")
106
+
107
+ try:
108
+ content = await safe_read_file(file)
109
+ except PermissionError:
110
+ return ErrorToolResult(
111
+ error_message=f"Permission denied: cannot read {tool_input.file_path}"
112
+ )
113
+ except UnicodeDecodeError:
114
+ return ErrorToolResult(
115
+ error_message="File appears to be binary or has invalid text encoding"
116
+ )
117
+ except Exception as e: # noqa: BLE001
118
+ return ErrorToolResult(error_message=f"Error reading file: {e!s}")
119
+
120
+ # Handle empty files
121
+ if not content:
122
+ return ReadToolResult(
123
+ content="",
124
+ num_lines=0,
125
+ start_line=0,
126
+ total_lines=0,
127
+ )
128
+
129
+ content_lines = content.splitlines(keepends=True)
130
+ total_lines = len(content_lines)
131
+
132
+ # Handle offset beyond file length
133
+ if offset >= total_lines:
134
+ return ReadToolResult(
135
+ content="",
136
+ num_lines=0,
137
+ start_line=offset,
138
+ total_lines=total_lines,
139
+ )
140
+
141
+ # Apply offset and limit
142
+ content_lines = content_lines[offset : offset + limit]
143
+
144
+ # Apply character-level truncation at line boundaries to ensure consistency
145
+ # This ensures the content field and num_lines field remain in sync
146
+ CHARACTER_LIMIT = 90_000 # Match the limit in truncation.py
147
+
148
+ # Join lines and check total size
149
+ final_content = "".join(content_lines)
150
+
151
+ if len(final_content) > CHARACTER_LIMIT:
152
+ # Truncate at line boundaries to stay under the limit
153
+ truncated_lines: list[str] = []
154
+ current_size = 0
155
+ truncation_message = "\n[Content truncated due to size limit]"
156
+ truncation_size = len(truncation_message)
157
+ lines_included = 0
158
+
159
+ for line in content_lines:
160
+ # Check if adding this line would exceed the limit (accounting for truncation message)
161
+ if current_size + len(line) + truncation_size > CHARACTER_LIMIT:
162
+ final_content = "".join(truncated_lines) + truncation_message
163
+ break
164
+ truncated_lines.append(line)
165
+ current_size += len(line)
166
+ lines_included += 1
167
+ else:
168
+ # All lines fit (shouldn't happen if we got here, but be safe)
169
+ final_content = "".join(truncated_lines)
170
+ lines_included = len(content_lines)
171
+
172
+ num_lines = lines_included
173
+ else:
174
+ num_lines = len(content_lines)
175
+
176
+ return ReadToolResult(
177
+ content=final_content,
178
+ num_lines=num_lines,
179
+ start_line=offset,
180
+ total_lines=total_lines,
181
+ )
182
+
183
+
184
+ async def execute_write_file(
185
+ tool_input: WriteToolInput, working_directory: str
186
+ ) -> WriteToolResult:
187
+ file_path = tool_input.file_path
188
+ path = Path(working_directory, file_path)
189
+ result = await execute_full_file_rewrite(
190
+ path, tool_input.content, working_directory
191
+ )
192
+ return WriteToolResult(message=result)
193
+
194
+
195
+ async def execute_list_files(
196
+ tool_input: ListToolInput, working_directory: str
197
+ ) -> ListToolResult | ErrorToolResult:
198
+ path = AsyncPath(tool_input.path)
199
+
200
+ try:
201
+ exists = await path.exists()
202
+ except (OSError, PermissionError) as e:
203
+ return ErrorToolResult(error_message=f"Cannot access path: {e!s}")
204
+
205
+ if not exists:
206
+ return ErrorToolResult(error_message=f"Directory not found: {tool_input.path}")
207
+
208
+ try:
209
+ is_dir = await path.is_dir()
210
+ except (OSError, PermissionError) as e:
211
+ return ErrorToolResult(
212
+ error_message=f"Cannot check if path is directory: {e!s}"
213
+ )
214
+
215
+ if not is_dir:
216
+ return ErrorToolResult(
217
+ error_message=f"Path is not a directory: {tool_input.path}"
218
+ )
219
+
220
+ try:
221
+ filenames = [entry.name async for entry in path.iterdir()]
222
+ except (OSError, PermissionError) as e:
223
+ return ErrorToolResult(error_message=f"Cannot list directory contents: {e!s}")
224
+
225
+ return ListToolResult(
226
+ files=[filename for filename in filenames],
227
+ )
228
+
229
+
230
+ async def execute_glob_files(
231
+ tool_input: GlobToolInput, working_directory: str
232
+ ) -> GlobToolResult:
233
+ # async timer
234
+ start_time = time()
235
+ results = await files.glob(
236
+ path=working_directory if tool_input.path is None else tool_input.path,
237
+ glob_pattern=tool_input.pattern,
238
+ )
239
+ duration_ms = int((time() - start_time) * 1000)
240
+ return GlobToolResult(
241
+ filenames=results,
242
+ duration_ms=duration_ms,
243
+ num_files=len(results),
244
+ truncated=len(results) >= files.GLOB_MAX_COUNT,
245
+ )
246
+
247
+
248
+ async def execute_grep_files(
249
+ tool_input: GrepToolInput, working_directory: str
250
+ ) -> GrepToolResult:
251
+ results = await files.search_files(
252
+ path_str=working_directory if tool_input.path is None else tool_input.path,
253
+ file_pattern=tool_input.include,
254
+ regex=tool_input.pattern,
255
+ working_directory=working_directory,
256
+ )
257
+ return GrepToolResult(
258
+ matches=results[:GREP_MAX_RESULTS],
259
+ truncated=bool(len(results) > GREP_MAX_RESULTS),
260
+ )
261
+
262
+
263
+ async def execute_bash_tool(
264
+ tool_input: BashToolInput, working_directory: str, should_halt: Callable[[], bool]
265
+ ) -> BashToolResult:
266
+ start_time = time()
267
+ result = None
268
+ async for result in execute_code_streaming(
269
+ StreamingCodeExecutionRequest(
270
+ language="shell",
271
+ content=tool_input.command,
272
+ timeout=120 if tool_input.timeout is None else tool_input.timeout,
273
+ correlation_id=str(uuid.uuid4()),
274
+ ),
275
+ working_directory=working_directory,
276
+ session=None, # type: ignore
277
+ should_halt=should_halt,
278
+ ):
279
+ pass
280
+
281
+ assert isinstance(result, StreamingCodeExecutionResponse)
282
+
283
+ return BashToolResult(
284
+ shell_output=result.content,
285
+ exit_code=result.exit_code,
286
+ duration_ms=int((time() - start_time) * 1000),
287
+ timed_out=result.cancelled_for_timeout,
288
+ stopped_by_user=result.halted,
289
+ )
@@ -0,0 +1,284 @@
1
+ """Generalized truncation framework for tool results."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, TypeVar, cast
5
+
6
+ from msgspec.structs import replace
7
+
8
+ from exponent.core.remote_execution.cli_rpc_types import (
9
+ BashToolResult,
10
+ ErrorToolResult,
11
+ GlobToolResult,
12
+ GrepToolResult,
13
+ ListToolResult,
14
+ ReadToolResult,
15
+ ToolResult,
16
+ WriteToolResult,
17
+ )
18
+ from exponent.core.remote_execution.utils import truncate_output
19
+
20
+ DEFAULT_CHARACTER_LIMIT = 90_000
21
+ DEFAULT_LIST_ITEM_LIMIT = 1000
22
+ DEFAULT_LIST_PREVIEW_ITEMS = 10
23
+
24
+
25
+ class TruncationStrategy(ABC):
26
+ @abstractmethod
27
+ def should_truncate(self, result: ToolResult) -> bool:
28
+ pass
29
+
30
+ @abstractmethod
31
+ def truncate(self, result: ToolResult) -> ToolResult:
32
+ pass
33
+
34
+
35
+ class StringFieldTruncation(TruncationStrategy):
36
+ def __init__(
37
+ self,
38
+ field_name: str,
39
+ character_limit: int = DEFAULT_CHARACTER_LIMIT,
40
+ ):
41
+ self.field_name = field_name
42
+ self.character_limit = character_limit
43
+
44
+ def should_truncate(self, result: ToolResult) -> bool:
45
+ if hasattr(result, self.field_name):
46
+ value = getattr(result, self.field_name)
47
+ if isinstance(value, str):
48
+ return len(value) > self.character_limit
49
+ return False
50
+
51
+ def truncate(self, result: ToolResult) -> ToolResult:
52
+ if not hasattr(result, self.field_name):
53
+ return result
54
+
55
+ value = getattr(result, self.field_name)
56
+ if not isinstance(value, str):
57
+ return result
58
+
59
+ truncated_value, was_truncated = truncate_output(value, self.character_limit)
60
+
61
+ updates: dict[str, Any] = {self.field_name: truncated_value}
62
+ if hasattr(result, "truncated") and was_truncated:
63
+ updates["truncated"] = True
64
+
65
+ return replace(result, **updates)
66
+
67
+
68
+ class ListFieldTruncation(TruncationStrategy):
69
+ def __init__(
70
+ self,
71
+ field_name: str,
72
+ item_limit: int = DEFAULT_LIST_ITEM_LIMIT,
73
+ preview_items: int = DEFAULT_LIST_PREVIEW_ITEMS,
74
+ ):
75
+ self.field_name = field_name
76
+ self.item_limit = item_limit
77
+ self.preview_items = preview_items
78
+
79
+ def should_truncate(self, result: ToolResult) -> bool:
80
+ if hasattr(result, self.field_name):
81
+ value = getattr(result, self.field_name)
82
+ if isinstance(value, list):
83
+ return len(value) > self.item_limit
84
+ return False
85
+
86
+ def truncate(self, result: ToolResult) -> ToolResult:
87
+ if not hasattr(result, self.field_name):
88
+ return result
89
+
90
+ value = getattr(result, self.field_name)
91
+ if not isinstance(value, list):
92
+ return result
93
+
94
+ total_items = len(value)
95
+ if total_items <= self.item_limit:
96
+ return result
97
+
98
+ truncated_count = max(0, total_items - 2 * self.preview_items)
99
+ truncated_list = (
100
+ value[: self.preview_items]
101
+ + [f"... {truncated_count} items truncated ..."]
102
+ + value[-self.preview_items :]
103
+ )
104
+
105
+ updates: dict[str, Any] = {self.field_name: truncated_list}
106
+ if hasattr(result, "truncated"):
107
+ updates["truncated"] = True
108
+
109
+ return replace(result, **updates)
110
+
111
+
112
+ class CompositeTruncation(TruncationStrategy):
113
+ def __init__(self, strategies: list[TruncationStrategy]):
114
+ self.strategies = strategies
115
+
116
+ def should_truncate(self, result: ToolResult) -> bool:
117
+ return any(strategy.should_truncate(result) for strategy in self.strategies)
118
+
119
+ def truncate(self, result: ToolResult) -> ToolResult:
120
+ for strategy in self.strategies:
121
+ if strategy.should_truncate(result):
122
+ result = strategy.truncate(result)
123
+ return result
124
+
125
+
126
+ class StringListTruncation(TruncationStrategy):
127
+ """Truncation for lists of strings that limits both number of items and individual string length."""
128
+
129
+ def __init__(
130
+ self,
131
+ field_name: str,
132
+ max_items: int = DEFAULT_LIST_ITEM_LIMIT,
133
+ preview_items: int = DEFAULT_LIST_PREVIEW_ITEMS,
134
+ max_item_length: int = 1000,
135
+ ):
136
+ self.field_name = field_name
137
+ self.max_items = max_items
138
+ self.preview_items = preview_items
139
+ self.max_item_length = max_item_length
140
+
141
+ def should_truncate(self, result: ToolResult) -> bool:
142
+ if not hasattr(result, self.field_name):
143
+ return False
144
+
145
+ items = getattr(result, self.field_name)
146
+ if not isinstance(items, list):
147
+ return False
148
+
149
+ # Check if we need to truncate number of items
150
+ if len(items) > self.max_items:
151
+ return True
152
+
153
+ # Check if any individual item is too long
154
+ for item in items:
155
+ if isinstance(item, str) and len(item) > self.max_item_length:
156
+ return True
157
+ # Handle dict items (e.g., with metadata like file path and line number)
158
+ elif isinstance(item, dict) and "content" in item:
159
+ if len(item["content"]) > self.max_item_length:
160
+ return True
161
+
162
+ return False
163
+
164
+ def _truncate_item_content(
165
+ self, item: str | dict[str, Any]
166
+ ) -> str | dict[str, Any]:
167
+ """Truncate an individual item's content."""
168
+ if isinstance(item, str):
169
+ if len(item) <= self.max_item_length:
170
+ return item
171
+ # Truncate string item
172
+ truncated, _ = truncate_output(item, self.max_item_length)
173
+ return truncated
174
+ elif isinstance(item, dict) and "content" in item:
175
+ # Handle dict-style items (e.g., with metadata like file path and line number)
176
+ if len(item["content"]) <= self.max_item_length:
177
+ return item
178
+ truncated_content, _ = truncate_output(
179
+ item["content"], self.max_item_length
180
+ )
181
+ return {**item, "content": truncated_content}
182
+ else:
183
+ return item
184
+
185
+ def truncate(self, result: ToolResult) -> ToolResult:
186
+ if not hasattr(result, self.field_name):
187
+ return result
188
+
189
+ items = getattr(result, self.field_name)
190
+ if not isinstance(items, list):
191
+ return result
192
+
193
+ # First, truncate individual item contents
194
+ truncated_items = [self._truncate_item_content(item) for item in items]
195
+
196
+ # Then, limit the number of items if needed
197
+ total_items = len(truncated_items)
198
+ if total_items > self.max_items:
199
+ truncated_count = max(0, total_items - 2 * self.preview_items)
200
+ final_items = (
201
+ truncated_items[: self.preview_items]
202
+ + [f"... {truncated_count} items truncated ..."]
203
+ + truncated_items[-self.preview_items :]
204
+ )
205
+ else:
206
+ final_items = truncated_items
207
+
208
+ updates: dict[str, Any] = {self.field_name: final_items}
209
+ if hasattr(result, "truncated"):
210
+ updates["truncated"] = True
211
+
212
+ return replace(result, **updates)
213
+
214
+
215
+ TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
216
+ ReadToolResult: StringFieldTruncation("content"),
217
+ WriteToolResult: StringFieldTruncation("message"),
218
+ BashToolResult: StringFieldTruncation("shell_output"),
219
+ GrepToolResult: StringListTruncation("matches"),
220
+ GlobToolResult: StringListTruncation("filenames", max_item_length=4096),
221
+ ListToolResult: StringListTruncation("files", max_item_length=4096),
222
+ }
223
+
224
+
225
+ T = TypeVar("T", bound=ToolResult)
226
+
227
+
228
+ def truncate_tool_result(result: T) -> T:
229
+ if isinstance(result, ErrorToolResult):
230
+ return cast(T, result)
231
+
232
+ result_type = type(result)
233
+ if result_type in TRUNCATION_REGISTRY:
234
+ strategy = TRUNCATION_REGISTRY[result_type]
235
+ if strategy.should_truncate(result):
236
+ return cast(T, strategy.truncate(result))
237
+
238
+ return result
239
+
240
+
241
+ def register_truncation_strategy(
242
+ result_type: type[ToolResult],
243
+ strategy: TruncationStrategy,
244
+ ) -> None:
245
+ TRUNCATION_REGISTRY[result_type] = strategy
246
+
247
+
248
+ def configure_truncation_limits(
249
+ character_limit: int | None = None,
250
+ list_item_limit: int | None = None,
251
+ list_preview_items: int | None = None,
252
+ string_item_length: int | None = None,
253
+ ) -> None:
254
+ for strategy in TRUNCATION_REGISTRY.values():
255
+ if isinstance(strategy, StringFieldTruncation) and character_limit is not None:
256
+ strategy.character_limit = character_limit
257
+ elif isinstance(strategy, ListFieldTruncation):
258
+ if list_item_limit is not None:
259
+ strategy.item_limit = list_item_limit
260
+ if list_preview_items is not None:
261
+ strategy.preview_items = list_preview_items
262
+ elif isinstance(strategy, StringListTruncation):
263
+ if list_item_limit is not None:
264
+ strategy.max_items = list_item_limit
265
+ if list_preview_items is not None:
266
+ strategy.preview_items = list_preview_items
267
+ if string_item_length is not None:
268
+ strategy.max_item_length = string_item_length
269
+ elif isinstance(strategy, CompositeTruncation):
270
+ for sub_strategy in strategy.strategies:
271
+ if isinstance(sub_strategy, StringFieldTruncation) and character_limit:
272
+ sub_strategy.character_limit = character_limit
273
+ elif isinstance(sub_strategy, ListFieldTruncation):
274
+ if list_item_limit is not None:
275
+ sub_strategy.item_limit = list_item_limit
276
+ if list_preview_items is not None:
277
+ sub_strategy.preview_items = list_preview_items
278
+ elif isinstance(sub_strategy, StringListTruncation):
279
+ if list_item_limit is not None:
280
+ sub_strategy.max_items = list_item_limit
281
+ if list_preview_items is not None:
282
+ sub_strategy.preview_items = list_preview_items
283
+ if string_item_length is not None:
284
+ sub_strategy.max_item_length = string_item_length