Coverage for src / renaissance / utils / text_utils.py: 54%
99 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-09 14:04 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-09 14:04 +0000
1import re
2import subprocess
3import sys
4import tempfile
5from pathlib import Path
7import pyperclip
10class TextUtils:
11 __PRECEDING_SPACES_PATTERN = re.compile(r"([\t\s]*)")
13 @staticmethod
14 def shift_left(text: str, shift: int, start_line: int = 0) -> str:
15 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
16 if shift == 0:
17 return text
18 pattern = re.compile(r"\s{0," + str(shift) + "}(.*)")
19 lines = text.split("\n")
20 for idx, line in enumerate(lines[start_line:]):
21 lines[idx + start_line] = pattern.sub(r"\1", line)
22 return "\n".join(lines)
24 @staticmethod
25 def correct_indent(text: str, indent: int, depth: int = 0) -> str:
26 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
27 lines = text.split("\n")
28 for idx, line in enumerate(lines):
29 depth -= line.count("}")
30 stripped = re.sub(r"^\s*", "", line)
31 lines[idx] = " " * depth * indent + stripped if stripped else stripped
32 depth += line.count("{")
34 return "\n".join(lines)
36 @staticmethod
37 def strip_indent(text: str, start_line: int = 0) -> str:
38 """Shifts left the text such that the first line has no leading spaces and all other lines shifted left
39 with the first line spaces length.
40 """
41 matcher = TextUtils.__PRECEDING_SPACES_PATTERN.search(text)
42 if matcher:
43 spaces = matcher[1]
44 text = TextUtils.shift_left(text, len(spaces), start_line)
45 return text.strip()
47 @staticmethod
48 def shift_right(text: str, shift: int, start_line: int = 0) -> str:
49 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
50 if shift == 0:
51 return text
52 lines = text.split("\n")
53 spaces = " " * shift
54 for idx, line in enumerate(lines[start_line:]):
55 lines[idx + start_line] = spaces + line if line else line
56 return "\n".join(lines)
58 @staticmethod
59 def get_indent(content: bytes, offset: int) -> int:
60 """Calculate the indentation level of a line in a byte string.
62 Args:
63 content (bytes): The byte string containing the text.
64 offset (int): The position within the byte string to start calculating the indentation from.
66 Returns:
67 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset.
69 """
70 indent = offset
71 while indent > 1:
72 if content[indent - 1] in b"\n\r":
73 break
74 indent -= 1
75 start_of_line = indent
76 while indent < offset:
77 if content[indent] not in b"\t ":
78 break
79 indent += 1
80 return indent - start_of_line
82 @staticmethod
83 def get_spaces_before(content: bytes, offset: int) -> int:
84 """Calculate the indentation level of a line in a byte string.
86 Args:
87 content (bytes): The byte string containing the text.
88 offset (int): The position within the byte string to start calculating the indentation from.
90 Returns:
91 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset.
93 """
94 indent = offset - 1
95 while indent > 0:
96 if content[indent] not in b" \t":
97 break
98 indent -= 1
99 return offset - indent - 1
101 @staticmethod
102 def to_clipboard(text: str) -> None:
103 pyperclip.copy(text)
105 @staticmethod
106 def to_file(filename: str, text: str) -> None:
107 """Write the given text to a file with the specified filename."""
108 with Path(filename).open("w") as f:
109 f.write(text)
112def signature2id(signature: str) -> str:
113 text = signature.replace("\n", " ")
114 return re.sub(r"[^\w\s]", "", text)[:30] # Remove punctuation, limit length
117def camel_case(snippet: str) -> str:
118 parts = snippet.split("_")
119 return parts[0] + "".join(word.capitalize() for word in parts[1:])
122def snake_case(snippet: str) -> str:
123 # TODO: Why is exactly one non-capital character allowed in the second group?
124 # Is the regex correct? Should it be [A-Z][a-z]* or [A-Z][a-z]+ instead of [A-Z][a-z]?
125 return re.sub(r"([A-Z][A-Za-z]+)([A-Z][a-z])", r"\1_\2", snippet).lower()
128def fix_indent(code_string: str) -> str | None:
129 with tempfile.NamedTemporaryFile(suffix=".py", mode="w+", delete=False) as temp_file:
130 file_path = temp_file.name
131 temp_file.write(code_string)
133 try:
134 if not Path(file_path).is_file():
135 print(f"Error: {file_path} does not exist.")
136 return None
138 # Step 1: Run flake8 to show issues
139 print("Running flake8...")
140 subprocess.run([sys.executable, "-m", "flake8", file_path])
142 # Step 2: Auto-fix with autopep8
143 print("Auto-fixing with autopep8...")
144 subprocess.run(
145 [
146 sys.executable,
147 "-m",
148 "autopep8",
149 "--in-place",
150 "--aggressive",
151 "--aggressive",
152 file_path,
153 ],
154 )
156 # Step 3: Run flake8 again to verify
157 print("Re-running flake8 after fixes...")
158 subprocess.run([sys.executable, "-m", "flake8", file_path])
160 # Read the fixed code
161 with Path(file_path).open() as file:
162 fixed_code = file.read()
164 # black format
165 # return format_str(fixed_code, mode=FileMode())
166 return fixed_code
167 except Exception as e:
168 print(f"Error formatting code: {e}")
169 finally:
170 # Clean up the temporary file
171 if Path(file_path).exists():
172 Path(file_path).unlink()