|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +merge_repo.py — Merge Python source files from the 'src' subfolder into a single file. |
| 4 | +
|
| 5 | +Usage: |
| 6 | + python merge_repo.py [--root <repo_root>] [--out <merged_file>] [--folder src] |
| 7 | +
|
| 8 | +Defaults: |
| 9 | + --root . |
| 10 | + --out merged_repo.py |
| 11 | + --folder src |
| 12 | +
|
| 13 | +Behavior: |
| 14 | +- Recursively collects *.py files under the given subfolder (relative to --root). |
| 15 | +- Preserves per-file content verbatim (opened as UTF-8 with replacement for undecodable bytes). |
| 16 | +- Inserts a clear header block before each file containing: |
| 17 | + * Original relative path within the repository |
| 18 | + * A best-effort import path (dot-separated) relative to the repo root |
| 19 | + * Hints extracted from env.sh if present (e.g., PYTHONPATH changes) |
| 20 | +
|
| 21 | +Notes: |
| 22 | +- File order is stable and sorted (case-insensitive) by relative path. |
| 23 | +- __init__.py files are included (they can document package structure). |
| 24 | +- If duplicate module names occur, they are still included in distinct sections. |
| 25 | +- The merged file starts with a table of contents to aid navigation. |
| 26 | +""" |
| 27 | + |
| 28 | +import os, sys, argparse |
| 29 | + |
| 30 | +def read_env_sh(repo_root: str) -> str: |
| 31 | + for base, dirs, files in os.walk(repo_root): |
| 32 | + if 'env.sh' in files: |
| 33 | + p = os.path.join(base, 'env.sh') |
| 34 | + try: |
| 35 | + with open(p, 'r', encoding='utf-8', errors='replace') as f: |
| 36 | + return f.read() |
| 37 | + except Exception: |
| 38 | + return '' |
| 39 | + return '' |
| 40 | + |
| 41 | +def guess_import_path(rel_path: str) -> str: |
| 42 | + if rel_path.endswith('.py'): |
| 43 | + rel_path = rel_path[:-3] |
| 44 | + rel_path = rel_path.replace('\\', '/') |
| 45 | + parts = [p for p in rel_path.split('/') if p not in ('.', '')] |
| 46 | + return '.'.join(parts) |
| 47 | + |
| 48 | +def collect_files(repo_root: str, folder: str): |
| 49 | + wanted = [] |
| 50 | + base = os.path.join(repo_root, folder) |
| 51 | + if not os.path.isdir(base): |
| 52 | + return wanted |
| 53 | + for root, dirs, files in os.walk(base): |
| 54 | + dirs[:] = [d for d in dirs if d not in ('__pycache__', '.git', '.mypy_cache', '.pytest_cache', '.venv', 'venv')] |
| 55 | + for fn in files: |
| 56 | + if fn.endswith('.py'): |
| 57 | + full = os.path.join(root, fn) |
| 58 | + rel = os.path.relpath(full, repo_root) |
| 59 | + wanted.append(rel) |
| 60 | + wanted.sort(key=lambda s: s.lower()) |
| 61 | + return wanted |
| 62 | + |
| 63 | +def main(): |
| 64 | + ap = argparse.ArgumentParser() |
| 65 | + ap.add_argument('--root', default='.', help='Repository root (default: .)') |
| 66 | + ap.add_argument('--out', default='merged_repo.py', help='Output merged file path') |
| 67 | + ap.add_argument('--folder', default='src', help='Subfolder to include (default: src)') |
| 68 | + args = ap.parse_args() |
| 69 | + |
| 70 | + repo_root = os.path.abspath(args.root) |
| 71 | + env_text = read_env_sh(repo_root) |
| 72 | + files = collect_files(repo_root, args.folder) |
| 73 | + |
| 74 | + header_lines = [] |
| 75 | + header_lines.append('#' * 80) |
| 76 | + header_lines.append('# MERGED REPOSITORY FILE (auto-generated by merge_repo.py)') |
| 77 | + header_lines.append('#') |
| 78 | + header_lines.append('# Included subfolder: ' + args.folder) |
| 79 | + header_lines.append('# Repository root: ' + repo_root) |
| 80 | + header_lines.append('# Total files: ' + str(len(files))) |
| 81 | + header_lines.append('#') |
| 82 | + header_lines.append('# Table of Contents (relative path -> import path):') |
| 83 | + for rel in files: |
| 84 | + header_lines.append('# - {rel} -> {imp}'.format(rel=rel, imp=guess_import_path(rel))) |
| 85 | + header_lines.append('#') |
| 86 | + if env_text.strip(): |
| 87 | + header_lines.append('# env.sh (excerpt, first 60 lines):') |
| 88 | + lines = env_text.splitlines() |
| 89 | + for line in lines[:60]: |
| 90 | + header_lines.append('# ' + line) |
| 91 | + if len(lines) > 60: |
| 92 | + header_lines.append('# ... (truncated)') |
| 93 | + else: |
| 94 | + header_lines.append('# No env.sh found or it was empty/unreadable.') |
| 95 | + header_lines.append('#' * 80) |
| 96 | + header_lines.append('') |
| 97 | + merged_header = '\n'.join(header_lines) |
| 98 | + |
| 99 | + os.makedirs(os.path.dirname(os.path.abspath(args.out)) or '.', exist_ok=True) |
| 100 | + with open(args.out, 'w', encoding='utf-8', errors='replace') as out: |
| 101 | + out.write(merged_header + '\n\n') |
| 102 | + for rel in files: |
| 103 | + imp = guess_import_path(rel) |
| 104 | + section_header = [] |
| 105 | + section_header.append('\n' + '#' * 80) |
| 106 | + section_header.append('# BEGIN FILE: {rel}'.format(rel=rel)) |
| 107 | + section_header.append('# Suggested import: {imp}'.format(imp=imp)) |
| 108 | + if env_text.strip(): |
| 109 | + section_header.append('# env.sh hints: the environment may export PYTHONPATH or other variables') |
| 110 | + section_header.append('# that affect how modules are imported. See the env.sh excerpt above.') |
| 111 | + section_header.append('# Additional context:') |
| 112 | + section_header.append('# - This block is a verbatim copy of the original file.') |
| 113 | + section_header.append('# - If this file defines __all__ or relies on package-relative imports, the order in') |
| 114 | + section_header.append('# this merged file does not simulate runtime import side-effects; it is for reading.') |
| 115 | + section_header.append('# - Use the relative path to locate the original source in the repo.') |
| 116 | + section_header.append('#' * 80 + '\n') |
| 117 | + out.write('\n'.join(section_header)) |
| 118 | + |
| 119 | + full = os.path.join(repo_root, rel) |
| 120 | + try: |
| 121 | + with open(full, 'r', encoding='utf-8', errors='replace') as f: |
| 122 | + content = f.read() |
| 123 | + except Exception as e: |
| 124 | + content = '# ERROR reading file: {e}\n'.format(e=e) |
| 125 | + out.write(content) |
| 126 | + out.write('\n\n# END FILE: {rel}\n'.format(rel=rel)) |
| 127 | + |
| 128 | +if __name__ == '__main__': |
| 129 | + main() |
0 commit comments