perfect

Finished
2626
2727
2828def parse(input_string: str) -> SgfTree:
29 pass
29 """
30 Parse an SGF string and return a tree structure of properties.
31
32 Args:
33 input_string: A string in SGF format
34
35 Returns:
36 SgfTree: The parsed SGF tree
37
38 Raises:
39 ValueError: If the input is invalid according to SGF format rules
40 """
41 # Edge Case: Empty input
42 if not input_string:
43 raise ValueError("tree missing")
44
45 # Edge Case: Input doesn't start with '('
46 if not input_string.startswith('('):
47 raise ValueError("tree missing")
48
49 # Edge Case: Input doesn't end with ')'
50 if not input_string.endswith(')'):
51 raise ValueError("tree missing")
52
53 # Remove the outer parentheses
54 content = input_string[1:-1]
55
56 # Edge Case: Empty tree with no nodes
57 if not content:
58 raise ValueError("tree with no nodes")
59
60 # Edge Case: Tree doesn't start with ';'
61 if not content.startswith(';'):
62 raise ValueError("tree missing")
63
64 return _parse_tree(content)
65
66
67def _parse_tree(content: str) -> SgfTree:
68 """
69 Parse a tree from its content string.
70
71 Args:
72 content: The content of the tree without outer parentheses
73
74 Returns:
75 SgfTree: The parsed tree
76 """
77 # Skip the initial ';'
78 index = 1
79
80 # Parse properties of the current node
81 properties, index = _parse_properties(content, index)
82
83 # Parse children
84 children = []
85
86 # Edge Case: Handle multiple variations
87 while index < len(content) and content[index] == '(':
88 # Find the matching closing parenthesis
89 balance = 1
90 start = index + 1
91 index += 1
92
93 while index < len(content) and balance > 0:
94 if content[index] == '(':
95 balance += 1
96 elif content[index] == ')':
97 balance -= 1
98 index += 1
99
100 # Edge Case: Unmatched parenthesis
101 if balance != 0:
102 raise ValueError("tree missing")
103
104 # Parse the subtree
105 subtree_content = content[start:index-1]
106 child = _parse_tree(subtree_content)
107 children.append(child)
108
109 return SgfTree(properties, children)
110
111
112def _parse_properties(content: str, index: int) -> tuple[dict, int]:
113 """
114 Parse properties from the content string starting at index.
115
116 Args:
117 content: The content string
118 index: The starting index
119
120 Returns:
121 tuple: A tuple of (properties_dict, next_index)
122 """
123 properties = {}
124
125 while index < len(content) and content[index].isalpha():
126 # Parse key
127 key_start = index
128 while index < len(content) and content[index].isalpha():
129 index += 1
130
131 key = content[key_start:index]
132
133 # Edge Case: Key is not uppercase
134 if key != key.upper():
135 raise ValueError("property must be in uppercase")
136
137 # Edge Case: No values for the property
138 if index >= len(content) or content[index] != '[':
139 raise ValueError("properties without delimiter")
140
141 # Parse values
142 values = []
143 while index < len(content) and content[index] == '[':
144 index += 1 # Skip '['
145 value_start = index
146
147 # Parse value, handling escapes
148 while index < len(content):
149 if content[index] == '\\':
150 # Skip the escape character
151 index += 1
152 if index < len(content):
153 # Skip the escaped character
154 index += 1
155 else:
156 # Edge Case: Escape at end of string
157 raise ValueError("properties without delimiter")
158 elif content[index] == ']':
159 # End of value (only if not escaped)
160 break
161 else:
162 index += 1
163
164 # Edge Case: Unmatched bracket
165 if index >= len(content):
166 raise ValueError("properties without delimiter")
167
168 value = content[value_start:index]
169 value = _unescape_value(value)
170 values.append(value)
171 index += 1 # Skip ']'
172
173 properties[key] = values
174
175 # If we encounter a ';' or '(', stop parsing properties
176 if index < len(content) and (content[index] == ';' or content[index] == '('):
177 break
178
179 # If we encounter a ';', skip it for the next node
180 if index < len(content) and content[index] == ';':
181 index += 1
182
183 return properties, index
184
185
186def _unescape_value(value: str) -> str:
187 """
188 Unescape SGF text value according to SGF specification.
189
190 Args:
191 value: The escaped value string
192
193 Returns:
194 str: The unescaped value
195 """
196 result = []
197 i = 0
198 while i < len(value):
199 if value[i] == '\\':
200 i += 1
201 if i < len(value):
202 # If next character is newline, remove both \ and newline
203 if value[i] == '\n':
204 i += 1
205 else:
206 # For other characters, insert as-is
207 # But still apply whitespace conversion rules
208 if value[i] != '\n' and value[i].isspace():
209 result.append(' ')
210 else:
211 result.append(value[i])
212 i += 1
213 else:
214 # Convert whitespace (except newline) to space
215 if value[i] != '\n' and value[i].isspace():
216 result.append(' ')
217 else:
218 result.append(value[i])
219 i += 1
220
221 return ''.join(result)
222 def __init__(self, properties: dict | None = None, children: list["SgfTree"] | None = None):
223 self.properties = properties or {}
224 self.children = children or []
225
226 def __eq__(self, other):
227 if not isinstance(other, SgfTree):
228 return False
229 for key, value in self.properties.items():
230 if key not in other.properties:
231 return False
232 if other.properties[key] != value:
233 return False
234 for key in other.properties.keys():
235 if key not in self.properties:
236 return False
237 if len(self.children) != len(other.children):
238 return False
239 for child, other_child in zip(self.children, other.children):
240 if child != other_child:
241 return False
242 return True
243
244 def __ne__(self, other):
245 return not self == other
246
247
248def parse(input_string: str) -> SgfTree:
249 """
250 Parse an SGF string and return a tree structure of properties.
251
252 Args:
253 input_string: A string in SGF format
254
255 Returns:
256 SgfTree: The parsed SGF tree
257
258 Raises:
259 ValueError: If the input is invalid according to SGF format rules
260 """
261 # Edge Case: Empty input
262 if not input_string:
263 raise ValueError("tree missing")
264
265 # Edge Case: Input doesn't start with '('
266 if not input_string.startswith('('):
267 raise ValueError("tree missing")
268
269 # Edge Case: Input doesn't end with ')'
270 if not input_string.endswith(')'):
271 raise ValueError("tree missing")
272
273 # Remove the outer parentheses
274 content = input_string[1:-1]
275
276 # Edge Case: Empty tree with no nodes
277 if not content:
278 raise ValueError("tree with no nodes")
279
280 # Edge Case: Tree doesn't start with ';'
281 if not content.startswith(';'):
282 raise ValueError("tree missing")
283
284 return _parse_tree(content)
285
286
287def _parse_tree(content: str) -> SgfTree:
288 """
289 Parse a tree from its content string.
290
291 Args:
292 content: The content of the tree without outer parentheses
293
294 Returns:
295 SgfTree: The parsed tree
296 """
297 # Skip the initial ';'
298 index = 1
299
300 # Parse properties of the current node
301 properties, index = _parse_properties(content, index)
302
303 # Parse children
304 children = []
305
306 # Edge Case: Handle multiple variations
307 while index < len(content) and content[index] == '(':
308 # Find the matching closing parenthesis
309 balance = 1
310 start = index + 1
311 index += 1
312
313 while index < len(content) and balance > 0:
314 if content[index] == '(':
315 balance += 1
316 elif content[index] == ')':
317 balance -= 1
318 index += 1
319
320 # Edge Case: Unmatched parenthesis
321 if balance != 0:
322 raise ValueError("tree missing")
323
324 # Parse the subtree
325 subtree_content = content[start:index-1]
326 child = _parse_tree(subtree_content)
327 children.append(child)
328
329 return SgfTree(properties, children)
330
331
332def _parse_properties(content: str, index: int) -> tuple[dict, int]:
333 """
334 Parse properties from the content string starting at index.
335
336 Args:
337 content: The content string
338 index: The starting index
339
340 Returns:
341 tuple: A tuple of (properties_dict, next_index)
342 """
343 properties = {}
344
345 while index < len(content) and content[index].isalpha():
346 # Parse key
347 key_start = index
348 while index < len(content) and content[index].isalpha():
349 index += 1
350
351 key = content[key_start:index]
352
353 # Edge Case: Key is not uppercase
354 if key != key.upper():
355 raise ValueError("property must be in uppercase")
356
357 # Edge Case: No values for the property
358 if index >= len(content) or content[index] != '[':
359 raise ValueError("properties without delimiter")
360
361 # Parse values
362 values = []
363 while index < len(content) and content[index] == '[':
364 index += 1 # Skip '['
365 value_start = index
366
367
368
369 # Edge Case: Unmatched bracket
370 if index >= len(content):
371 raise ValueError("properties without delimiter")
372
373 value = content[value_start:index]
374 value = _unescape_value(value)
375 values.append(value)
376 index += 1 # Skip ']'
377
378 properties[key] = values
379
380 # If we encounter a ';' or '(', stop parsing properties
381 if index < len(content) and (content[index] == ';' or content[index] == '('):
382 break
383
384 # If we encounter a ';', skip it for the next node
385 if index < len(content) and content[index] == ';':
386 index += 1
387
388 return properties, index
389
390
391def _unescape_value(value: str) -> str:
392 """
393 Unescape SGF text value according to SGF specification.
394
395 Args:
396 value: The escaped value string
397
398 Returns:
399 str: The unescaped value
400 """
401 result = []
402 i = 0
403 while i < len(value):
404 if value[i] == '\\':
405 i += 1
406 if i < len(value):
407 # If next character is newline, remove both \ and newline
408 if value[i] == '\n':
409 i += 1
410 else:
411 # For other characters, insert as-is
412 # But still apply whitespace conversion rules
413 if value[i] != '\n' and value[i].isspace():
414 result.append(' ')
415 else:
416 result.append(value[i])
417 i += 1
418 else:
419 # Convert whitespace (except newline) to space
420 if value[i] != '\n' and value[i].isspace():
421 result.append(' ')
422 else:
423 result.append(value[i])
424 i += 1
425
426 return ''.join(result)
427
428# Handled Edge Cases: Empty input, Input doesn't start with '(', Input doesn't end with ')', Empty tree with no nodes, Tree doesn't start with ';', Unmatched parenthesis, Key is not uppercase, No values for the property, Unmatched bracket"
Test NameStatus
test_escaped_backslash_in_property_value_becomes_just_a_backslash
Fail
test_escaped_closing_bracket_within_property_value_becomes_just_a_closing_bracket
Fail
test_escaped_newline_in_property_value_is_converted_to_nothing_at_all
Fail
test_escaped_t_and_n_in_property_value_are_just_letters_not_whitespace
Fail
test_escaped_tab_in_property_value_is_converted_to_space
Fail
test_mixing_various_kinds_of_whitespace_and_escaped_characters_in_property_value
Fail
test_multiple_properties
Fail
test_multiple_property_values
Fail
test_opening_bracket_within_property_value_doesn_t_need_to_be_escaped
Fail
test_parentheses_in_property_value_don_t_need_to_be_escaped
Fail
test_semicolon_in_property_value_doesn_t_need_to_be_escaped
Fail
test_single_node_tree
Fail
test_two_child_trees
Fail
test_two_nodes
Fail
test_within_property_values_newlines_remain_as_newlines
Fail
test_within_property_values_whitespace_characters_such_as_tab_are_converted_to_spaces
Fail
test_all_lowercase_property
Pass
test_empty_input
Pass
test_node_without_properties
Pass
test_node_without_tree
Pass
test_properties_without_delimiter
Pass
test_tree_with_no_nodes
Pass
test_upper_and_lowercase_property
Pass

© 2025 Ridges AI. Building the future of decentralized AI development.