-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathonefile.py
More file actions
218 lines (186 loc) · 7.74 KB
/
onefile.py
File metadata and controls
218 lines (186 loc) · 7.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import ast
import os
import sys
import re
FOLDER = "src"
OUTPUT_FILE = "quadratic-invaders.py"
def merge_python_files(src_folder):
"""
Merge all Python files in src_folder into a single output.
Handles imports deduplication, removal of internal imports,
and class ordering by dependencies.
Made by ChatGPT with few tweaks
"""
# 1. Retrieve all .py files
py_files = []
for root, dirs, files in os.walk(src_folder):
for name in files:
if name.endswith('.py'):
py_files.append(os.path.join(root, name))
py_files.sort() # Sort for a deterministic order
# names of internal modules (.py files)
internal_modules = {os.path.splitext(os.path.basename(p))[0] for p in py_files}
all_imports = set() # Set to deduplicate external imports
classes = {} # Dictionary class -> {code, deps}
functions = {} # name -> {code, deps}
other_code_lines = [] # Lines of code that are not imports and not classes
# 2. Process each file: collect imports and class definitions
for filepath in py_files:
with open(filepath, 'r', encoding='utf-8') as f:
code = f.read().rstrip('\n')
try:
tree = ast.parse(code, filepath)
except SyntaxError as e:
print(f"SyntaxError in {filepath}: {e}", file=sys.stderr)
continue
code_lines = code.splitlines()
class_lines = set()
for node in tree.body:
# External imports -> keep, internal imports -> ignore
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name not in internal_modules: # keep only if external module
all_imports.add(code_lines[node.lineno - 1].strip())
elif isinstance(node, ast.ImportFrom):
module = node.module.split('.')[0] if node.module else ""
if module not in internal_modules: # keep only if external module
all_imports.add(code_lines[node.lineno - 1].strip())
elif isinstance(node, ast.ClassDef):
# Extract the class block safely (handle nodes without end_lineno)
start = node.lineno - 1
end = getattr(node, "end_lineno", None)
if end is None:
# Try to infer end by taking the max end_lineno/lineno of child nodes
child_lines = []
for child in ast.walk(node):
ln = getattr(child, "end_lineno", None) or getattr(child, "lineno", None)
if isinstance(ln, int):
child_lines.append(ln)
if child_lines:
end = max(child_lines)
else:
# Fallback: assume class is a single-line definition
end = start + 1
# Ensure end is an int and within bounds for slicing
if not isinstance(end, int):
end = start + 1
end = min(end, len(code_lines))
class_block = "\n".join(code_lines[start:end])
classes[node.name] = {"code": class_block, "deps": set()}
class_lines.update(range(start, end))
elif isinstance(node, ast.FunctionDef):
start = node.lineno - 1
end = getattr(node, "end_lineno", None)
if end is None:
child_lines = []
for child in ast.walk(node):
ln = getattr(child, "end_lineno", None) or getattr(child, "lineno", None)
if isinstance(ln, int):
child_lines.append(ln)
end = max(child_lines) if child_lines else start + 1
end = min(end, len(code_lines))
func_block = "\n".join(code_lines[start:end])
functions[node.name] = {"code": func_block, "deps": set()}
class_lines.update(range(start, end))
# Collect the rest of the code (functions, main script, etc.)
for i, line in enumerate(code_lines):
stripped = line.strip()
if stripped.startswith('import ') or stripped.startswith('from '):
continue # skip all imports
if i in class_lines:
continue # skip class lines
other_code_lines.append(line)
other_code_lines.append('') # separation between files
# 3. Sort external imports
sorted_imports = sorted(all_imports)
# 4. Detect dependencies between classes via ast.walk
class_names = set(classes.keys())
for cls, info in classes.items():
try:
class_tree = ast.parse(info["code"])
except SyntaxError:
continue
for node in ast.walk(class_tree):
# Inheritance
if isinstance(node, ast.ClassDef):
for base in node.bases:
if isinstance(base, ast.Name) and base.id in class_names:
info["deps"].add(base.id)
# Direct references to other classes
elif isinstance(node, ast.Name):
if node.id in class_names and node.id != cls:
info["deps"].add(node.id)
func_names = set(functions.keys())
for fn, info in functions.items():
try:
func_tree = ast.parse(info["code"])
except SyntaxError:
continue
for node in ast.walk(func_tree):
if isinstance(node, ast.Name):
if node.id in func_names and node.id != fn:
info["deps"].add(node.id)
# 5. Topological sorting of classes
sorted_classes = []
deps = {cls: set(info["deps"]) for cls, info in classes.items()}
while deps:
acyclic = False
for cls, dset in list(deps.items()):
dset.discard(cls)
if not dset:
acyclic = True
sorted_classes.append(cls)
del deps[cls]
for other in list(deps):
deps[other].discard(cls)
if not acyclic: # cycle → append the rest as-is
sorted_classes.extend(deps.keys())
break
sorted_functions = []
deps_f = {fn: set(info["deps"]) for fn, info in functions.items()}
while deps_f:
acyclic = False
for fn, dset in list(deps_f.items()):
dset.discard(fn)
if not dset:
acyclic = True
sorted_functions.append(fn)
del deps_f[fn]
for other in list(deps_f):
deps_f[other].discard(fn)
if not acyclic:
sorted_functions.extend(deps_f.keys())
break
# 6. Build the final code
output = []
# External imports
if sorted_imports:
output.extend(sorted_imports)
output.append("")
# Ordered classes
for cls in sorted_classes:
output.append(classes[cls]["code"])
output.append("")
# Ordered functions
for fn in sorted_functions:
output.append(functions[fn]["code"])
output.append("")
# Other lines of code
if other_code_lines:
output.extend(other_code_lines)
return "\n".join(output)
def write(filename, content):
"""
Write the file and remove unnecessary blank lines.
Made by ChatGPT with few tweaks
"""
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
with open(filename) as f_input:
data = f_input.read().rstrip('\n')
data = re.sub(r'\n\s*\n', '\n\n', data)
with open(filename, 'w') as f_output:
f_output.write(data)
if __name__ == "__main__":
merged = merge_python_files(FOLDER)
write(OUTPUT_FILE, merged)