Selfie
Loading...
Searching...
No Matches
SourceFile.py
Go to the documentation of this file.
1from typing import Any
2
3from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
4from .Literals import Language, LiteralFormat, LiteralRepr, LiteralValue
5from .Slice import Slice
6
7
9 TRIPLE_QUOTE = '"""'
10
11 def __init__(self, filename: str, content: str) -> None:
12 self.__unix_newlines: bool = "\r" not in content
13 self._content_slice: Slice = Slice(content.replace("\r\n", "\n"))
14 self.__language: Language = Language.from_filename(filename)
15 self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for(
16 self._content_slice.__str__()
17 )
18
20 # Split content into lines
21 lines = self._content_slice.__str__().split("\n")
22
23 # Create a new list of lines, excluding lines containing '# selfieonce' or '#selfieonce'
24 new_lines = []
25 for line in lines:
26 # Check for both variations of the comment
27 if "# selfieonce" in line:
28 cleaned_line = line.split("# selfieonce")[0].strip()
29 elif "#selfieonce" in line:
30 cleaned_line = line.split("#selfieonce")[0].strip()
31 else:
32 new_lines.append(line)
33 continue
34
35 # If the line has code before the comment, keep the code part
36 if cleaned_line:
37 new_lines.append(cleaned_line)
38
39 # Recombine the lines into a single string
40 new_content = "\n".join(new_lines)
41
42 # Update the content slice with new content
43 self._content_slice = Slice(new_content)
44
45 if not self.__unix_newlines:
46 self._content_slice = Slice(new_content.replace("\n", "\r\n"))
47
48 @property
49 def as_string(self) -> str:
50 return (
51 self._content_slice.__str__()
52 if self.__unix_newlines
53 else self._content_slice.__str__().replace("\n", "\r\n")
54 )
55
58 self,
59 parent: "SourceFile",
60 dot_fun_open_paren: str,
61 function_call_plus_arg: Slice,
62 arg: Slice,
63 language: Language,
64 escape_leading_whitespace: EscapeLeadingWhitespace,
65 ) -> None:
66 self.__parent = parent
67 self.__dot_fun_open_paren = dot_fun_open_paren
68 self.__function_call_plus_arg = function_call_plus_arg
69 self.__arg = arg
70 self.__language = language
71 self.__escape_leading_whitespace = escape_leading_whitespace
72
76 def _get_arg(self):
77 return self.__arg
78
79 def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int:
80 encoded = literal_value.format.encode(
81 literal_value.actual,
82 self.__language,
84 )
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 )
100 existing_newlines = self.__function_call_plus_arg.count("\n")
101 new_newlines = encoded.count("\n")
102 self.__parent._content_slice = Slice( # noqa: SLF001
103 self.__function_call_plus_arg.replaceSelfWith(
104 f"{self.__dot_fun_open_paren}{encoded})"
105 )
106 )
107
108 return new_newlines - existing_newlines
109
110 def parse_literal(self, literal_format: LiteralFormat) -> Any:
111 return literal_format.parse(self.__arg.__str__(), self.__language)
112
113 def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice:
114 line_content = self._content_slice.unixLine(line_one_indexed)
115 idx = line_content.indexOf(to_find)
116 if idx == -1:
117 raise AssertionError(
118 f"Expected to find `{to_find}` on line {line_one_indexed}, but there was only `{line_content}`"
119 )
120 return line_content.subSequence(idx, idx + len(to_find))
121
122 def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None:
123 assert "\n" not in find
124 assert "\n" not in replace
125
126 found = self.find_on_line(find, line_one_indexed)
127 self._content_slice = Slice(found.replaceSelfWith(replace))
128
129 def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
130 line_content = self._content_slice.unixLine(line_one_indexed)
131 dot_fun_open_paren = None
132
133 for to_be_like in TO_BE_LIKES:
134 idx = line_content.indexOf(to_be_like)
135 if idx != -1:
136 dot_fun_open_paren = to_be_like
137 break
138 if dot_fun_open_paren is None:
139 raise AssertionError(
140 f"Expected to find inline assertion on line {line_one_indexed}, but there was only `{line_content}`"
141 )
142
143 dot_function_call_in_place = line_content.indexOf(dot_fun_open_paren)
144 dot_function_call = dot_function_call_in_place + line_content.startIndex
145 arg_start = dot_function_call + len(dot_fun_open_paren)
146
147 if self._content_slice.__len__() == arg_start:
148 raise AssertionError(
149 f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
150 f"on line {line_one_indexed}"
151 )
152 while self._content_slice[arg_start].isspace():
153 arg_start += 1
154 if self._content_slice.__len__() == arg_start:
155 raise AssertionError(
156 f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
157 f"on line {line_one_indexed}"
158 )
159
160 if self._content_slice[arg_start] == '"':
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,
175 )
176
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
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
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_QUOTETRIPLE_QUOTE):
228 end_arg = self._content_slice.indexOf(
229 self.TRIPLE_QUOTETRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTETRIPLE_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}"
235 )
236 else:
237 end_arg += len(self.TRIPLE_QUOTETRIPLE_QUOTE)
238 end_paren = end_arg
239 else:
240 end_arg = arg_start + 1
241 while (
242 self._content_slice[end_arg] != '"'
243 or self._content_slice[end_arg - 1] == "\\"
244 ):
245 end_arg += 1
246 if end_arg == self._content_slice.__len__():
247 raise AssertionError(
248 f'Appears to be an unclosed string literal `"` '
249 f"on line {line_one_indexed}"
250 )
251 end_arg += 1
252 end_paren = end_arg
253 while self._content_slice[end_paren] != ")":
254 if not self._content_slice[end_paren].isspace():
255 raise AssertionError(
256 f"Non-primitive literal in `{dot_fun_open_paren}` starting at "
257 f"line {line_one_indexed}: error for character "
258 f"`{self._content_slice[end_paren]}` on line "
259 f"{self._content_slice.baseLineAtOffset(end_paren)}"
260 )
261 end_paren += 1
262 if end_paren == self._content_slice.__len__():
263 raise AssertionError(
264 f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
265 f"starting at line {line_one_indexed}"
266 )
267 return (end_paren, end_arg)
268
269
270TO_BE_LIKES = [
271 ".to_be(",
272 ".to_be_TODO(",
273 ".to_be_base64(",
274 ".to_be_base64_TODO(",
275 ".to_be_TODO(",
276]
Any parse_literal(self, LiteralFormat literal_format)
int set_literal_and_get_newline_delta(self, LiteralValue literal_value)
Definition SourceFile.py:79
None __init__(self, "SourceFile" parent, str dot_fun_open_paren, Slice function_call_plus_arg, Slice arg, Language language, EscapeLeadingWhitespace escape_leading_whitespace)
Definition SourceFile.py:65
ToBeLiteral parse_to_be_like(self, int line_one_indexed)
None __init__(self, str filename, str content)
Definition SourceFile.py:11
_parse_string(self, int line_one_indexed, int arg_start, str dot_fun_open_paren)
_parse_code(self, int line_one_indexed, int arg_start, str dot_fun_open_paren)
Slice find_on_line(self, str to_find, int line_one_indexed)
None replace_on_line(self, int line_one_indexed, str find, str replace)