141 *, no_diagnostics: bool =
False) -> str:
142 """Reconstruct a class definition with methods and nested classes."""
151 bases =
", ".join(ast.unparse(b)
for b
in node.bases)
152 keywords =
", ".join(ast.unparse(kw)
for kw
in node.keywords)
153 all_args =
", ".join(filter(
None, [bases, keywords]))
154 sig = f
"{indent}class {node.name}({all_args}):" if all_args
else f
"{indent}class {node.name}:"
158 docstring = ast.get_docstring(node, clean=
False)
162 parts.append(f
'{indent} """{docstring}"""')
164 inner_indent = indent +
" "
167 for child
in node.body:
169 if (isinstance(child, ast.Expr)
and isinstance(child.value, ast.Constant)
170 and isinstance(child.value.value, str)
and not has_body
171 and ast.get_docstring(node, clean=
False)):
175 if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
179 elif isinstance(child, ast.ClassDef):
182 no_diagnostics=no_diagnostics))
184 elif isinstance(child, ast.AnnAssign)
and child.target:
185 parts.append(f
"{inner_indent}{ast.unparse(child)}")
187 elif isinstance(child, ast.Assign):
189 names = [t.id
for t
in child.targets
190 if isinstance(t, ast.Name)
and t.id.lstrip(
"_").isupper()]
192 parts.append(f
"{inner_indent}{' = '.join(names)} = ...")
196 parts.append(f
"{inner_indent}...")
198 return "\n".join(parts)
202 no_diagnostics: bool =
False) -> str:
203 """Extract structural skeleton from a Python file's source text.
205 Returns a string containing headers, module docstring, and
206 class/function signatures — no implementation bodies.
208 lines = text.splitlines()
209 parts: list[str] = []
212 header_lines = lines[:3]
if len(lines) >= 3
else lines[:]
213 parts.append(
"\n".join(header_lines))
216 tree, _warnings = safe_parse(text)
219 parts.append(
"# [syntax error — file could not be parsed]")
220 return "\n".join(parts) +
"\n"
223 docstring = ast.get_docstring(tree, clean=
False)
228 parts.append(f
'"""{docstring}"""')
231 for node
in tree.body:
233 if (isinstance(node, ast.Expr)
and isinstance(node.value, ast.Constant)
234 and isinstance(node.value.value, str)
and docstring):
237 if isinstance(node, ast.ClassDef):
239 parts.append(
_format_class(node, no_diagnostics=no_diagnostics))
240 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
243 elif isinstance(node, ast.AnnAssign)
and node.target:
244 target_name = getattr(node.target,
"id",
"")
245 if target_name.lstrip(
"_").isupper():
247 parts.append(ast.unparse(node))
248 elif isinstance(node, ast.Assign):
249 names = [t.id
for t
in node.targets
250 if isinstance(t, ast.Name)
and t.id.lstrip(
"_").isupper()]
253 parts.append(f
"{' = '.join(names)} = ...")
255 return "\n".join(parts) +
"\n"
271 no_diagnostics: bool =
False) -> tuple:
272 """Write a skeleton export file for all source files in a directory."""
273 files = collect_recursive_files(folder, cfg)
281 fh = output_path.open(
"w", encoding=
"utf-8")
283 print(f
" Warning: cannot write '{output_path.name}': {e}")
284 return 0, 0, 0, 0, files
287 out.write(_HEADER_TEXT.format(directory=folder))
288 out.write(_ROOT_HEADER)
290 for idx, file_path
in enumerate(files, start=1):
291 rel_path = file_path.relative_to(folder)
292 out.write(_SEPARATOR_FILE.format(num=idx, path=str(rel_path)))
294 if file_path.suffix ==
".py":
296 text = file_path.read_text(encoding=
"utf-8", errors=
"replace")
297 except (OSError, UnicodeDecodeError):
298 print(f
"Warning: Could not read {file_path}: skipping",
302 no_diagnostics=no_diagnostics)
310 total_bytes += file_path.stat().st_size
312 content = file_path.read_text(encoding=
"utf-8", errors=
"replace")
313 total_lines += content.count(
"\n") + 1
314 total_words += len(content.split())
315 except (OSError, UnicodeDecodeError):
318 return file_count, total_lines, total_words, total_bytes, files
351 json_mode: bool =
False,
353 """Execute the skeleton exporter with the given root directory and arguments.
355 When *json_mode* is True, emits a machine-readable JSON summary to stdout
356 instead of the plain-text summary:
357 ``{"tool": "export-skeleton", "directories_processed": N,
358 "files_exported": N, "output_dirs": [...], "exit_code": 0}``
360 do_clean =
"--clean" in args
361 verbose =
"--verbose" in args
362 single_dir_mode =
"--single-dir" in args
363 no_diagnostics =
"--no-diagnostics" in args
369 cfg = load_exporter_config(root)
371 timestamp = datetime.now().strftime(
"%Y%m%d-%H%M")
372 root_name = root.name
374 summary: Dict[str, Tuple[int, int, int, int]] = {}
375 per_dir_files: Dict[str, List[Path]] = {}
376 skipped: List[str] = []
378 single_out_root: Path |
None =
None
380 single_out_root = root / f
"_skeleton_code-{timestamp}"
382 single_out_root.mkdir(parents=
True, exist_ok=
True)
384 print(f
" Error: cannot create single output dir: {e}")
388 for folder
in walk_included_dirs(root, cfg):
389 if should_skip_dirname(folder.name, cfg):
393 rel_to_root = folder.relative_to(root)
if folder != root
else Path(
".")
394 out_dir = single_out_root / rel_to_root
396 out_dir = folder / f
"_skeleton_code-{timestamp}"
399 out_dir.mkdir(parents=
True, exist_ok=
True)
401 rel_err =
"." if folder == root
else str(folder.relative_to(root))
402 print(f
" Warning: cannot create output dir for '{folder.name}': {e}")
403 skipped.append(rel_err)
406 rel =
"." if folder == root
else str(folder.relative_to(root))
407 dotpath = root_name
if folder == root
else f
"{root_name}.{rel.replace(os.sep, '.')}"
408 out_path = _make_safe_output_path(out_dir, dotpath, cfg,
412 file_count, total_lines, total_words, total_bytes, files = (
414 no_diagnostics=no_diagnostics)
416 except Exception
as e:
417 print(f
" Warning: export failed for '{rel}': {e}")
421 summary[rel] = (file_count, total_lines, total_words, total_bytes)
422 per_dir_files[rel] = files
426 root_out_dir = single_out_root
428 root_out_dir = root / f
"_skeleton_code-{timestamp}"
429 full_output_path = _make_safe_output_path(root_out_dir, root_name, cfg,
430 prefix=
"_full_skeleton")
432 all_files = collect_recursive_files(root, cfg)
433 grouped: Dict[str, List[Path]] = {}
435 parent_rel = p.parent.relative_to(root)
436 key =
"." if str(parent_rel) ==
"." else str(parent_rel)
437 grouped.setdefault(key, []).append(p)
440 fh = full_output_path.open(
"w", encoding=
"utf-8")
442 print(f
" Warning: cannot write full-skeleton file: {e}")
447 out.write(_HEADER_TEXT.format(directory=root))
448 out.write(_ROOT_HEADER)
450 for idx_dir, dirname
in enumerate(sorted(grouped.keys()), start=1):
451 out.write(_SEPARATOR_SUBDIR.format(num=idx_dir, dirname=dirname))
452 for idx_file, file_path
in enumerate(grouped[dirname], start=1):
453 rel_path = file_path.relative_to(root)
454 out.write(_SEPARATOR_FILE.format(num=idx_file,
457 if file_path.suffix ==
".py":
459 text = file_path.read_text(encoding=
"utf-8",
461 except (OSError, UnicodeDecodeError):
464 no_diagnostics=no_diagnostics))
472 total_files, total_lines, total_words, total_bytes = summary[
"."]
474 total_files = total_lines = total_words = total_bytes = 0
476 flines, fwords, fbytes = get_file_stats(p)
478 total_lines += flines
479 total_words += fwords
480 total_bytes += fbytes
484 output_dirs: List[str] = []
485 if single_dir_mode
and single_out_root
is not None:
486 output_dirs = [str(single_out_root)]
489 str(root / f
"_skeleton_code-{timestamp}"),
492 "tool":
"export-skeleton",
493 "directories_processed": len(summary),
494 "files_exported": total_files,
495 "total_lines": total_lines,
496 "total_words": total_words,
497 "total_bytes": total_bytes,
498 "output_dirs": output_dirs,
503 print(f
"\n========== SKELETON SUMMARY ==========")
504 seen_files_v: set[Path] = set()
505 for dirname
in sorted(summary.keys()):
506 files_count, lines, words, bytes_ = summary[dirname]
508 print(f
"Directory '{dirname}': {files_count} files, "
509 f
"{lines} lines, {words} words, {kb:.1f} KB")
511 for p
in per_dir_files.get(dirname, []):
512 if p
not in seen_files_v:
514 flines, fwords, fbytes = get_file_stats(p)
515 fkb = fbytes / 1024.0
516 rel_path = p.relative_to(root)
517 print(f
" {rel_path} -- {flines} lines, "
518 f
"{fwords} words, {fkb:.1f} KB")
519 print(f
"\nTotal directories processed: {len(summary)}")
520 print(f
"Total source files processed: {total_files}")
521 print(f
"Total lines (original): {total_lines}")
522 print(f
"Total words (original): {total_words}")
523 print(f
"Total size (original): {total_bytes / 1024.0:.1f} KB")
525 print(f
"Output stored in single directory: {single_out_root}")
527 print(f
"Output stored under _skeleton_code-{timestamp} directories")
530 print(f
"\nSkipped {n} director{'y' if n == 1 else 'ies'} due to errors:")
533 print(
"======================================\n")