Skip to content

Commit 187cc70

Browse files
authored
py: fix bugs related to switching a snapshot between string and repr (#385)
2 parents 107bbee + b2bfd19 commit 187cc70

File tree

5 files changed

+136
-122
lines changed

5 files changed

+136
-122
lines changed

Diff for: python/selfie-lib/selfie_lib/Literals.py

+23-34
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,22 @@ def parse(self, string: str, language: Language) -> T:
4444
PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1
4545

4646

47-
class LiteralInt(LiteralFormat[int]):
48-
def _encode_underscores(
49-
self, buffer: io.StringIO, value: int, language: Language
50-
) -> io.StringIO:
51-
if value >= MAX_RAW_NUMBER:
52-
mod = value % MAX_RAW_NUMBER
53-
left_padding = PADDING_SIZE - len(str(mod))
54-
self._encode_underscores(buffer, value // MAX_RAW_NUMBER, language)
55-
buffer.write("_")
56-
buffer.write("0" * left_padding)
57-
buffer.write(str(mod))
58-
return buffer
59-
elif value < 0:
60-
buffer.write("-")
61-
self._encode_underscores(buffer, abs(value), language)
62-
return buffer
63-
else:
64-
buffer.write(str(value))
65-
return buffer
66-
67-
def encode(
68-
self,
69-
value: int,
70-
language: Language,
71-
encoding_policy: EscapeLeadingWhitespace, # noqa: ARG002
72-
) -> str:
73-
return self._encode_underscores(io.StringIO(), value, language).getvalue()
74-
75-
def parse(self, string: str, language: Language) -> int: # noqa: ARG002
76-
return int(string.replace("_", ""))
47+
def _encode_int_underscores(buffer: io.StringIO, value: int) -> str:
48+
if value >= MAX_RAW_NUMBER:
49+
mod = value % MAX_RAW_NUMBER
50+
left_padding = PADDING_SIZE - len(str(mod))
51+
_encode_int_underscores(buffer, value // MAX_RAW_NUMBER)
52+
buffer.write("_")
53+
buffer.write("0" * left_padding)
54+
buffer.write(str(mod))
55+
return buffer.getvalue()
56+
elif value < 0:
57+
buffer.write("-")
58+
_encode_int_underscores(buffer, abs(value))
59+
return buffer.getvalue()
60+
else:
61+
buffer.write(str(value))
62+
return buffer.getvalue()
7763

7864

7965
TRIPLE_QUOTE = '"""'
@@ -252,15 +238,18 @@ def handle_escape_sequences(line: str) -> str:
252238

253239
class LiteralRepr(LiteralFormat[Any]):
254240
def encode(
255-
self, value: Any, language: Language, encoding_policy: EscapeLeadingWhitespace
241+
self,
242+
value: Any,
243+
language: Language, # noqa: ARG002
244+
encoding_policy: EscapeLeadingWhitespace, # noqa: ARG002
256245
) -> str:
257246
if isinstance(value, int):
258-
return LiteralInt().encode(value, language, encoding_policy)
247+
return _encode_int_underscores(io.StringIO(), value)
259248
else:
260249
return repr(value)
261250

262-
def parse(self, string: str, language: Language) -> Any: # noqa: ARG002
263-
return eval(string)
251+
def parse(self, string: str, language: Language) -> Any:
252+
raise NotImplementedError
264253

265254

266255
class TodoStub(Enum):

Diff for: python/selfie-lib/selfie_lib/SelfieImplementations.py

-5
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,6 @@ def to_be_TODO(self, _: Any = None) -> str:
158158

159159
def to_be(self, expected: str) -> str:
160160
actual_string = self.__actual()
161-
162-
# Check if expected is a string
163-
if not isinstance(expected, str):
164-
raise TypeError("Expected value must be a string")
165-
166161
if actual_string == expected:
167162
return _checkSrc(actual_string)
168163
else:

Diff for: python/selfie-lib/selfie_lib/SourceFile.py

+102-55
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any
22

33
from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
4-
from .Literals import Language, LiteralFormat, LiteralValue
4+
from .Literals import Language, LiteralFormat, LiteralRepr, LiteralValue
55
from .Slice import Slice
66

77

@@ -78,21 +78,25 @@ def _get_arg(self):
7878

7979
def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int:
8080
encoded = literal_value.format.encode(
81-
literal_value.actual, self.__language, self.__escape_leading_whitespace
81+
literal_value.actual,
82+
self.__language,
83+
self.__escape_leading_whitespace,
8284
)
83-
round_tripped = literal_value.format.parse(encoded, self.__language)
84-
if round_tripped != literal_value.actual:
85-
raise ValueError(
86-
f"There is an error in {literal_value.format.__class__.__name__}, "
87-
"the following value isn't round tripping.\n"
88-
f"Please report this error and the data below at "
89-
"https://github.com/diffplug/selfie/issues/new\n"
90-
f"```\n"
91-
f"ORIGINAL\n{literal_value.actual}\n"
92-
f"ROUNDTRIPPED\n{round_tripped}\n"
93-
f"ENCODED ORIGINAL\n{encoded}\n"
94-
f"```\n"
95-
)
85+
if not isinstance(literal_value.format, LiteralRepr):
86+
# we don't roundtrip LiteralRepr because `eval` is dangerous
87+
round_tripped = literal_value.format.parse(encoded, self.__language)
88+
if round_tripped != literal_value.actual:
89+
raise ValueError(
90+
f"There is an error in {literal_value.format.__class__.__name__}, "
91+
"the following value isn't round tripping.\n"
92+
f"Please report this error and the data below at "
93+
"https://github.com/diffplug/selfie/issues/new\n"
94+
f"```\n"
95+
f"ORIGINAL\n{literal_value.actual}\n"
96+
f"ROUNDTRIPPED\n{round_tripped}\n"
97+
f"ENCODED ORIGINAL\n{encoded}\n"
98+
f"```\n"
99+
)
96100
existing_newlines = self.__function_call_plus_arg.count("\n")
97101
new_newlines = encoded.count("\n")
98102
self.__parent._content_slice = Slice( # noqa: SLF001
@@ -153,48 +157,98 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
153157
f"on line {line_one_indexed}"
154158
)
155159

156-
end_arg = -1
157-
end_paren = 0
158160
if self._content_slice[arg_start] == '"':
159-
if self._content_slice.subSequence(
160-
arg_start, len(self._content_slice)
161-
).starts_with(self.TRIPLE_QUOTE):
162-
end_arg = self._content_slice.indexOf(
163-
self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE)
161+
(end_paren, end_arg) = self._parse_string(
162+
line_one_indexed, arg_start, dot_fun_open_paren
163+
)
164+
else:
165+
(end_paren, end_arg) = self._parse_code(
166+
line_one_indexed, arg_start, dot_fun_open_paren
167+
)
168+
return self.ToBeLiteral(
169+
self,
170+
dot_fun_open_paren.replace("_TODO", ""),
171+
self._content_slice.subSequence(dot_function_call, end_paren + 1),
172+
self._content_slice.subSequence(arg_start, end_arg),
173+
self.__language,
174+
self.__escape_leading_whitespace,
175+
)
176+
177+
def _parse_code(
178+
self,
179+
line_one_indexed: int,
180+
arg_start: int,
181+
dot_fun_open_paren: str,
182+
):
183+
# Initialize variables
184+
parenthesis_count = 1
185+
string_delimiter = None
186+
187+
# Iterate through the characters starting from the given index
188+
for i in range(arg_start, len(self._content_slice)):
189+
char = self._content_slice[i]
190+
191+
# Check if we are entering or leaving a string
192+
if char in ["'", '"'] and self._content_slice[i - 1] != "\\":
193+
if not string_delimiter:
194+
string_delimiter = char
195+
elif char == string_delimiter:
196+
string_delimiter = None
197+
198+
# Skip characters inside strings
199+
if string_delimiter:
200+
continue
201+
202+
# Count parentheses
203+
if char == "(":
204+
parenthesis_count += 1
205+
elif char == ")":
206+
parenthesis_count -= 1
207+
208+
# If all parentheses are closed, return the current index
209+
if parenthesis_count == 0:
210+
end_paren = i
211+
end_arg = i - 1
212+
return (end_paren, end_arg)
213+
# else ...
214+
raise AssertionError(
215+
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
216+
f"starting at line {line_one_indexed}"
217+
)
218+
219+
def _parse_string(
220+
self,
221+
line_one_indexed: int,
222+
arg_start: int,
223+
dot_fun_open_paren: str,
224+
):
225+
if self._content_slice.subSequence(
226+
arg_start, len(self._content_slice)
227+
).starts_with(self.TRIPLE_QUOTE):
228+
end_arg = self._content_slice.indexOf(
229+
self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE)
230+
)
231+
if end_arg == -1:
232+
raise AssertionError(
233+
f"Appears to be an unclosed multiline string literal `{self.TRIPLE_QUOTE}` "
234+
f"on line {line_one_indexed}"
164235
)
165-
if end_arg == -1:
166-
raise AssertionError(
167-
f"Appears to be an unclosed multiline string literal `{self.TRIPLE_QUOTE}` "
168-
f"on line {line_one_indexed}"
169-
)
170-
else:
171-
end_arg += len(self.TRIPLE_QUOTE)
172-
end_paren = end_arg
173236
else:
174-
end_arg = arg_start + 1
175-
while (
176-
self._content_slice[end_arg] != '"'
177-
or self._content_slice[end_arg - 1] == "\\"
178-
):
179-
end_arg += 1
180-
if end_arg == self._content_slice.__len__():
181-
raise AssertionError(
182-
f'Appears to be an unclosed string literal `"` '
183-
f"on line {line_one_indexed}"
184-
)
185-
end_arg += 1
237+
end_arg += len(self.TRIPLE_QUOTE)
186238
end_paren = end_arg
187239
else:
188-
end_arg = arg_start
189-
while not self._content_slice[end_arg].isspace():
190-
if self._content_slice[end_arg] == ")":
191-
break
240+
end_arg = arg_start + 1
241+
while (
242+
self._content_slice[end_arg] != '"'
243+
or self._content_slice[end_arg - 1] == "\\"
244+
):
192245
end_arg += 1
193246
if end_arg == self._content_slice.__len__():
194247
raise AssertionError(
195-
f"Appears to be an unclosed numeric literal "
248+
f'Appears to be an unclosed string literal `"` '
196249
f"on line {line_one_indexed}"
197250
)
251+
end_arg += 1
198252
end_paren = end_arg
199253
while self._content_slice[end_paren] != ")":
200254
if not self._content_slice[end_paren].isspace():
@@ -210,14 +264,7 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
210264
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
211265
f"starting at line {line_one_indexed}"
212266
)
213-
return self.ToBeLiteral(
214-
self,
215-
dot_fun_open_paren.replace("_TODO", ""),
216-
self._content_slice.subSequence(dot_function_call, end_paren + 1),
217-
self._content_slice.subSequence(arg_start, end_arg),
218-
self.__language,
219-
self.__escape_leading_whitespace,
220-
)
267+
return (end_paren, end_arg)
221268

222269

223270
TO_BE_LIKES = [

Diff for: python/selfie-lib/selfie_lib/WriteTracker.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Dict, Generic, List, Optional, TypeVar, cast
88

99
from .FS import FS
10-
from .Literals import LiteralTodoStub, LiteralValue, TodoStub
10+
from .Literals import LiteralString, LiteralTodoStub, LiteralValue, TodoStub
1111
from .SourceFile import SourceFile
1212
from .TypedPath import TypedPath
1313

@@ -175,7 +175,11 @@ def record(
175175

176176
file = layout.sourcefile_for_call(call.location)
177177

178-
if snapshot.expected is not None:
178+
if (
179+
snapshot.expected is not None
180+
and isinstance(snapshot.expected, str)
181+
and isinstance(snapshot.format, LiteralString)
182+
):
179183
content = SourceFile(file.name, layout.fs.file_read(file))
180184
try:
181185
snapshot = cast(LiteralValue, snapshot)

Diff for: python/selfie-lib/tests/LiteralInt_test.py

+5-26
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
from selfie_lib.EscapeLeadingWhitespace import EscapeLeadingWhitespace
2-
from selfie_lib.Literals import Language, LiteralInt
1+
import io
32

4-
5-
def _encode(value: int, expected: str):
6-
literal_int = LiteralInt()
7-
actual = literal_int.encode(value, Language.PYTHON, EscapeLeadingWhitespace.NEVER)
8-
assert actual == expected, f"Expected '{expected}', but got '{actual}'"
3+
from selfie_lib.Literals import _encode_int_underscores
94

105

11-
def _decode(value: str, expected: int):
12-
literal_int = LiteralInt()
13-
actual = literal_int.parse(value, Language.PYTHON)
14-
assert actual == expected, f"Expected '{expected}', but got '{actual}'"
6+
def _encode(value: int, expected: str):
7+
actual = _encode_int_underscores(io.StringIO(), value)
8+
assert actual == expected
159

1610

1711
class TestLiteralInt:
@@ -35,18 +29,3 @@ def test_encode(self):
3529
]
3630
for value, expected in test_cases:
3731
_encode(value, expected)
38-
39-
def test_decode(self):
40-
test_cases = [
41-
("0", 0),
42-
("1", 1),
43-
("-1", -1),
44-
("999", 999),
45-
("9_99", 999),
46-
("9_9_9", 999),
47-
("-999", -999),
48-
("-9_99", -999),
49-
("-9_9_9", -999),
50-
]
51-
for value, expected in test_cases:
52-
_decode(value, expected)

0 commit comments

Comments
 (0)