Skip to content
This repository was archived by the owner on Apr 4, 2024. It is now read-only.

Commit eb4560c

Browse files
authored
Creating SourceFile (#20)
2 parents 417eaf6 + 4e73dd7 commit eb4560c

File tree

5 files changed

+350
-0
lines changed

5 files changed

+350
-0
lines changed
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from enum import Enum, auto
2+
3+
4+
class EscapeLeadingWhitespace(Enum):
5+
NEVER = auto()
6+
7+
def escape_line(self, line: str, space: str, tab: str) -> str:
8+
return line
9+
10+
@staticmethod
11+
def appropriate_for(file_content: str) -> "EscapeLeadingWhitespace":
12+
return EscapeLeadingWhitespace.NEVER

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

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from enum import Enum, auto
2+
from typing import Protocol, TypeVar
3+
from abc import abstractmethod
4+
from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
5+
6+
T = TypeVar("T")
7+
8+
9+
class Language(Enum):
10+
PYTHON = auto()
11+
12+
@classmethod
13+
def from_filename(cls, filename: str) -> "Language":
14+
extension = filename.rsplit(".", 1)[-1]
15+
if extension == "py":
16+
return cls.PYTHON
17+
else:
18+
raise ValueError(f"Unknown language for file {filename}")
19+
20+
21+
class LiteralValue:
22+
def __init__(self, expected: T | None, actual: T, format: "LiteralFormat") -> None:
23+
self.expected = expected
24+
self.actual = actual
25+
self.format = format
26+
27+
28+
class LiteralFormat(Protocol[T]):
29+
@abstractmethod
30+
def encode(
31+
self, value: T, language: Language, encoding_policy: "EscapeLeadingWhitespace"
32+
) -> str:
33+
raise NotImplementedError("Subclasses must implement the encode method")
34+
35+
@abstractmethod
36+
def parse(self, string: str, language: Language) -> T:
37+
raise NotImplementedError("Subclasses must implement the parse method")
38+
39+
40+
MAX_RAW_NUMBER = 1000
41+
PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1
42+
43+
44+
class LiteralBoolean(LiteralFormat[bool]):
45+
def encode(
46+
self, value: bool, language: Language, encoding_policy: EscapeLeadingWhitespace
47+
) -> str:
48+
return str(value)
49+
50+
def __to_boolean_strict(self, string: str) -> bool:
51+
if string.lower() == "true":
52+
return True
53+
elif string.lower() == "false":
54+
return False
55+
else:
56+
raise ValueError("String is not a valid boolean representation: " + string)
57+
58+
def parse(self, string: str, language: Language) -> bool:
59+
return self.__to_boolean_strict(string)

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

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
from .Slice import Slice
2+
from .Literals import Language, LiteralFormat, LiteralValue
3+
from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
4+
from typing import Any
5+
6+
7+
class SourceFile:
8+
TRIPLE_QUOTE = '"""'
9+
10+
def __init__(self, filename: str, content: str) -> None:
11+
self.__unix_newlines: bool = "\r" not in content
12+
self.__content_slice: Slice = Slice(content.replace("\r\n", "\n"))
13+
self.__language: Language = Language.from_filename(filename)
14+
self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for(
15+
self.__content_slice.__str__()
16+
)
17+
18+
@property
19+
def as_string(self) -> str:
20+
return (
21+
self.__content_slice.__str__()
22+
if self.__unix_newlines
23+
else self.__content_slice.__str__().replace("\n", "\r\n")
24+
)
25+
26+
class ToBeLiteral:
27+
def __init__(
28+
self,
29+
dot_fun_open_paren: str,
30+
function_call_plus_arg: Slice,
31+
arg: Slice,
32+
language: Language,
33+
escape_leading_whitespace: EscapeLeadingWhitespace,
34+
) -> None:
35+
self.__dot_fun_open_paren = dot_fun_open_paren
36+
self.__function_call_plus_arg = function_call_plus_arg
37+
self.__arg = arg
38+
self.__language = language
39+
self.__escape_leading_whitespace = escape_leading_whitespace
40+
41+
def _get_function_call_plus_arg(self):
42+
return self.__function_call_plus_arg
43+
44+
def _get_arg(self):
45+
return self.__arg
46+
47+
def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int:
48+
encoded = literal_value.format.encode(
49+
literal_value.actual, self.__language, self.__escape_leading_whitespace
50+
)
51+
round_tripped = literal_value.format.parse(encoded, self.__language)
52+
if round_tripped != literal_value.actual:
53+
raise ValueError(
54+
f"There is an error in {literal_value.format.__class__.__name__}, "
55+
"the following value isn't round tripping.\n"
56+
f"Please report this error and the data below at "
57+
"https://github.com/diffplug/selfie/issues/new\n"
58+
f"```\n"
59+
f"ORIGINAL\n{literal_value.actual}\n"
60+
f"ROUNDTRIPPED\n{round_tripped}\n"
61+
f"ENCODED ORIGINAL\n{encoded}\n"
62+
f"```\n"
63+
)
64+
existing_newlines = self.__function_call_plus_arg.count("\n")
65+
new_newlines = encoded.count("\n")
66+
self.__content_slice = self.__function_call_plus_arg.replaceSelfWith(
67+
f"{self.__dot_fun_open_paren}{encoded})"
68+
)
69+
return new_newlines - existing_newlines
70+
71+
def parse_literal(self, literal_format: LiteralFormat) -> Any:
72+
return literal_format.parse(self.__arg.__str__(), self.__language)
73+
74+
def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice:
75+
line_content = self.__content_slice.unixLine(line_one_indexed)
76+
idx = line_content.indexOf(to_find)
77+
if idx == -1:
78+
raise AssertionError(
79+
f"Expected to find `{to_find}` on line {line_one_indexed}, "
80+
f"but there was only `{line_content}`"
81+
)
82+
start_index = idx
83+
end_index = idx + len(to_find)
84+
return line_content.subSequence(start_index, end_index)
85+
86+
def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None:
87+
assert "\n" not in find
88+
assert "\n" not in replace
89+
line_content = self.__content_slice.unixLine(line_one_indexed).__str__()
90+
new_content = line_content.replace(find, replace)
91+
self.__content_slice = Slice(self.__content_slice.replaceSelfWith(new_content))
92+
93+
def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
94+
line_content = self.__content_slice.unixLine(line_one_indexed)
95+
dot_fun_open_paren = None
96+
97+
for to_be_like in TO_BE_LIKES:
98+
idx = line_content.indexOf(to_be_like)
99+
if idx != -1:
100+
dot_fun_open_paren = to_be_like
101+
break
102+
if dot_fun_open_paren is None:
103+
raise AssertionError(
104+
f"Expected to find inline assertion on line {line_one_indexed}, but there was only `{line_content}`"
105+
)
106+
107+
dot_function_call_in_place = line_content.indexOf(dot_fun_open_paren)
108+
dot_function_call = dot_function_call_in_place + line_content.startIndex
109+
arg_start = dot_function_call + len(dot_fun_open_paren)
110+
111+
if self.__content_slice.__len__() == arg_start:
112+
raise AssertionError(
113+
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
114+
f"on line {line_one_indexed}"
115+
)
116+
while self.__content_slice[arg_start].isspace():
117+
arg_start += 1
118+
if self.__content_slice.__len__() == arg_start:
119+
raise AssertionError(
120+
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
121+
f"on line {line_one_indexed}"
122+
)
123+
124+
end_arg = -1
125+
end_paren = 0
126+
if self.__content_slice[arg_start] == '"':
127+
if self.__content_slice[arg_start].startswith(self.TRIPLE_QUOTE):
128+
end_arg = self.__content_slice.indexOf(
129+
self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE)
130+
)
131+
if end_arg == -1:
132+
raise AssertionError(
133+
f"Appears to be an unclosed multiline string literal `{self.TRIPLE_QUOTE}` "
134+
f"on line {line_one_indexed}"
135+
)
136+
else:
137+
end_arg += len(self.TRIPLE_QUOTE)
138+
end_paren = end_arg
139+
else:
140+
end_arg = arg_start + 1
141+
while (
142+
self.__content_slice[end_arg] != '"'
143+
or self.__content_slice[end_arg - 1] == "\\"
144+
):
145+
end_arg += 1
146+
if end_arg == self.__content_slice.__len__():
147+
raise AssertionError(
148+
f'Appears to be an unclosed string literal `"` '
149+
f"on line {line_one_indexed}"
150+
)
151+
end_arg += 1
152+
end_paren = end_arg
153+
else:
154+
end_arg = arg_start
155+
while not self.__content_slice[end_arg].isspace():
156+
if self.__content_slice[end_arg] == ")":
157+
break
158+
end_arg += 1
159+
if end_arg == self.__content_slice.__len__():
160+
raise AssertionError(
161+
f"Appears to be an unclosed numeric literal "
162+
f"on line {line_one_indexed}"
163+
)
164+
end_paren = end_arg
165+
while self.__content_slice[end_paren] != ")":
166+
if not self.__content_slice[end_paren].isspace():
167+
raise AssertionError(
168+
f"Non-primitive literal in `{dot_fun_open_paren}` starting at "
169+
f"line {line_one_indexed}: error for character "
170+
f"`{self.__content_slice[end_paren]}` on line "
171+
f"{self.__content_slice.baseLineAtOffset(end_paren)}"
172+
)
173+
end_paren += 1
174+
if end_paren == self.__content_slice.__len__():
175+
raise AssertionError(
176+
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
177+
f"starting at line {line_one_indexed}"
178+
)
179+
return self.ToBeLiteral(
180+
dot_fun_open_paren.replace("_TODO", ""),
181+
self.__content_slice.subSequence(dot_function_call, end_paren + 1),
182+
self.__content_slice.subSequence(arg_start, end_arg),
183+
self.__language,
184+
self.__escape_leading_whitespace,
185+
)
186+
187+
188+
TO_BE_LIKES = [".toBe(", ".toBe_TODO(", ".toBeBase64(", ".toBeBase64_TODO("]

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

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .LineReader import LineReader as LineReader
22
from .Slice import Slice as Slice
3+
from .SourceFile import SourceFile as SourceFile
34
from .PerCharacterEscaper import PerCharacterEscaper as PerCharacterEscaper

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

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from selfie_lib import SourceFile
2+
3+
4+
def python_test(source_raw, function_call_plus_arg_raw, arg_raw=""):
5+
source = source_raw.replace("'", '"')
6+
function_call_plus_arg = function_call_plus_arg_raw.replace("'", '"')
7+
arg = arg_raw.replace("'", '"')
8+
parsed = SourceFile("UnderTest.py", source)
9+
to_be_literal = parsed.parse_to_be_like(1)
10+
assert to_be_literal._get_function_call_plus_arg() == function_call_plus_arg
11+
assert to_be_literal._get_arg() == arg
12+
13+
14+
def python_test_error(source_raw, error_msg):
15+
try:
16+
python_test(source_raw, "unusedArg")
17+
except AssertionError as e:
18+
assert str(e) == error_msg
19+
20+
21+
def todo():
22+
python_test(".toBe_TODO()", ".toBe_TODO()", "")
23+
python_test(" .toBe_TODO() ", ".toBe_TODO()", "")
24+
python_test(" .toBe_TODO( ) ", ".toBe_TODO( )", "")
25+
python_test(" .toBe_TODO( \n ) ", ".toBe_TODO( \n )", "")
26+
27+
28+
def numeric():
29+
python_test(".toBe(7)", ".toBe(7)", "7")
30+
python_test(" .toBe(7)", ".toBe(7)", "7")
31+
python_test(".toBe(7) ", ".toBe(7)", "7")
32+
python_test(" .toBe(7) ", ".toBe(7)", "7")
33+
python_test(" .toBe( 7 ) ", ".toBe( 7 )", "7")
34+
python_test(" .toBe(\n7) ", ".toBe(\n7)", "7")
35+
python_test(" .toBe(7\n) ", ".toBe(7\n)", "7")
36+
37+
38+
def single_line_string():
39+
python_test(".toBe('7')", "'7'")
40+
python_test(".toBe('')", "''")
41+
python_test(".toBe( '' )", "''")
42+
python_test(".toBe( \n '' \n )", "''")
43+
python_test(".toBe( \n '78' \n )", "'78'")
44+
python_test(".toBe('\\'')", "'\\''")
45+
46+
47+
def multi_line_string():
48+
python_test(".toBe('''7''')", "'''7'''")
49+
python_test(".toBe(''' 7 ''')", "''' 7 '''")
50+
python_test(".toBe('''\n7\n''')", "'''\n7\n'''")
51+
python_test(".toBe(''' ' '' ' ''')", "''' ' '' ' '''")
52+
53+
54+
def error_unclosed():
55+
python_test_error(
56+
".toBe(", "Appears to be an unclosed function call `.toBe()` on line 1"
57+
)
58+
python_test_error(
59+
".toBe( \n ", "Appears to be an unclosed function call `.toBe()` on line 1"
60+
)
61+
python_test_error(
62+
".toBe_TODO(",
63+
"Appears to be an unclosed function call `.toBe_TODO()` on line 1",
64+
)
65+
python_test_error(
66+
".toBe_TODO( \n ",
67+
"Appears to be an unclosed function call `.toBe_TODO()` on line 1",
68+
)
69+
python_test_error(
70+
".toBe_TODO(')", 'Appears to be an unclosed string literal `"` on line 1'
71+
)
72+
python_test_error(
73+
".toBe_TODO(''')",
74+
'Appears to be an unclosed multiline string literal `"""` on line 1',
75+
)
76+
77+
78+
def error_non_primitive():
79+
python_test_error(
80+
".toBe(1 + 1)",
81+
"Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1",
82+
)
83+
python_test_error(
84+
".toBe('1' + '1')",
85+
"Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1",
86+
)
87+
python_test_error(
88+
".toBe('''1''' + '''1''')",
89+
"Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1",
90+
)

0 commit comments

Comments
 (0)