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

1import re 

2import subprocess 

3import sys 

4import tempfile 

5from pathlib import Path 

6 

7import pyperclip 

8 

9 

10class TextUtils: 

11 __PRECEDING_SPACES_PATTERN = re.compile(r"([\t\s]*)") 

12 

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) 

23 

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("{") 

33 

34 return "\n".join(lines) 

35 

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() 

46 

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) 

57 

58 @staticmethod 

59 def get_indent(content: bytes, offset: int) -> int: 

60 """Calculate the indentation level of a line in a byte string. 

61 

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. 

65 

66 Returns: 

67 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset. 

68 

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 

81 

82 @staticmethod 

83 def get_spaces_before(content: bytes, offset: int) -> int: 

84 """Calculate the indentation level of a line in a byte string. 

85 

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. 

89 

90 Returns: 

91 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset. 

92 

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 

100 

101 @staticmethod 

102 def to_clipboard(text: str) -> None: 

103 pyperclip.copy(text) 

104 

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) 

110 

111 

112def signature2id(signature: str) -> str: 

113 text = signature.replace("\n", " ") 

114 return re.sub(r"[^\w\s]", "", text)[:30] # Remove punctuation, limit length 

115 

116 

117def camel_case(snippet: str) -> str: 

118 parts = snippet.split("_") 

119 return parts[0] + "".join(word.capitalize() for word in parts[1:]) 

120 

121 

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() 

126 

127 

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) 

132 

133 try: 

134 if not Path(file_path).is_file(): 

135 print(f"Error: {file_path} does not exist.") 

136 return None 

137 

138 # Step 1: Run flake8 to show issues 

139 print("Running flake8...") 

140 subprocess.run([sys.executable, "-m", "flake8", file_path]) 

141 

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 ) 

155 

156 # Step 3: Run flake8 again to verify 

157 print("Re-running flake8 after fixes...") 

158 subprocess.run([sys.executable, "-m", "flake8", file_path]) 

159 

160 # Read the fixed code 

161 with Path(file_path).open() as file: 

162 fixed_code = file.read() 

163 

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()