Skip to content

Commit

Permalink
type hints, comments
Browse files Browse the repository at this point in the history
  • Loading branch information
abinthomasonline committed Aug 5, 2024
1 parent a6daf05 commit 8c45cdd
Show file tree
Hide file tree
Showing 19 changed files with 648 additions and 148 deletions.
8 changes: 7 additions & 1 deletion repopack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
from .cli import run_cli
from .version import __version__

__all__ = ["pack", "run_cli", "__version__"]
# Define the public API of the package
__all__: list[str] = ["pack", "run_cli", "__version__"]

# Type hints for imported objects
pack: callable
run_cli: callable
__version__: str
8 changes: 7 additions & 1 deletion repopack/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from .cli import run_cli

# This is the main entry point for the repopack command-line application.
# It checks if the script is being run directly (not imported as a module)
# and if so, it calls the run_cli function to start the CLI.

if __name__ == "__main__":
run_cli()
run_cli() # type: ignore
# Note: The type: ignore comment is added because run_cli is imported
# from a local module and mypy might not be able to infer its type.
23 changes: 18 additions & 5 deletions repopack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os
import sys
from typing import Dict, Any
from .packager import pack
from .config import load_config, merge_configs
from .exceptions import RepopackError, ConfigurationError
Expand All @@ -11,7 +12,12 @@
from .version import __version__


def run_cli():
def run_cli() -> None:
"""
Main entry point for the Repopack CLI.
Parses command-line arguments, loads and merges configurations, and executes the packing process.
"""
# Set up argument parser
parser = argparse.ArgumentParser(
description="Repopack - Pack your repository into a single AI-friendly file"
)
Expand All @@ -37,10 +43,12 @@ def run_cli():
)
args = parser.parse_args()

# Set verbosity level
logger.set_verbose(args.verbose)

# Load configuration
try:
config = load_config(args.config)
config: Dict[str, Any] = load_config(args.config)
except ConfigurationError as e:
logger.error(f"Configuration file error: {str(e)}")
logger.debug("Stack trace:", exc_info=True)
Expand All @@ -50,7 +58,8 @@ def run_cli():
logger.debug("Stack trace:", exc_info=True)
sys.exit(1)

cli_config = {}
# Create CLI configuration
cli_config: Dict[str, Any] = {}
if args.output:
cli_config["output"] = {"file_path": args.output}
if args.ignore:
Expand All @@ -65,21 +74,25 @@ def run_cli():
cli_config["output"] = cli_config.get("output", {})
cli_config["output"]["style"] = args.output_style

# Merge configurations
try:
merged_config = merge_configs(config, cli_config)
merged_config: Dict[str, Any] = merge_configs(config, cli_config)
except ConfigurationError as e:
logger.error(f"Error merging configurations: {str(e)}")
logger.debug("Stack trace:", exc_info=True)
sys.exit(1)

logger.debug(f"Merged configuration: {merged_config}")

# Initialize spinner for visual feedback
spinner = Spinner("Packing files...")
try:
spinner.start()
pack_result = pack(os.path.abspath(args.directory), merged_config)
# Execute packing process
pack_result: Dict[str, Any] = pack(os.path.abspath(args.directory), merged_config)
spinner.succeed("Packing completed successfully!")

# Print summary and completion message
print_summary(
pack_result["total_files"],
pack_result["total_characters"],
Expand Down
44 changes: 40 additions & 4 deletions repopack/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json
from typing import Dict, Any
from typing import Dict, Any, Optional
from .exceptions import ConfigurationError


DEFAULT_CONFIG = {
# Default configuration for Repopack
DEFAULT_CONFIG: Dict[str, Dict[str, Any]] = {
"output": {
"file_path": "repopack-output.txt",
"style": "plain",
Expand All @@ -20,7 +21,19 @@
}


def load_config(config_path: str = None) -> Dict[str, Any]:
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"""
Load configuration from a JSON file.
Args:
config_path (Optional[str]): Path to the configuration file.
Returns:
Dict[str, Any]: Loaded configuration or an empty dictionary if no file is provided.
Raises:
ConfigurationError: If there's an error reading or parsing the configuration file.
"""
if config_path:
try:
with open(config_path, "r") as f:
Expand All @@ -33,6 +46,19 @@ def load_config(config_path: str = None) -> Dict[str, Any]:


def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Merge configurations from different sources.
Args:
file_config (Dict[str, Any]): Configuration loaded from a file.
cli_config (Dict[str, Any]): Configuration provided via command-line interface.
Returns:
Dict[str, Any]: Merged configuration.
Raises:
ConfigurationError: If there's an error during the merging process.
"""
try:
merged = DEFAULT_CONFIG.copy()
merged = deep_merge(merged, file_config)
Expand All @@ -42,7 +68,17 @@ def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Di
raise ConfigurationError(f"Error merging configurations: {str(e)}")


def deep_merge(dict1, dict2):
def deep_merge(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively merge two dictionaries.
Args:
dict1 (Dict[str, Any]): First dictionary to merge.
dict2 (Dict[str, Any]): Second dictionary to merge.
Returns:
Dict[str, Any]: Merged dictionary.
"""
for key, value in dict2.items():
if key in dict1:
if isinstance(dict1[key], dict) and isinstance(value, dict):
Expand Down
39 changes: 35 additions & 4 deletions repopack/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
# file: exceptions.py

from typing import Optional


class RepopackError(Exception):
"""Base exception class for Repopack errors."""

pass
def __init__(self, message: Optional[str] = None) -> None:
"""
Initialize the RepopackError.
Args:
message (Optional[str]): The error message. Defaults to None.
"""
super().__init__(message)


class ConfigurationError(RepopackError):
"""Raised when there's an error in the configuration."""

pass
def __init__(self, message: str) -> None:
"""
Initialize the ConfigurationError.
Args:
message (str): The specific configuration error message.
"""
super().__init__(f"Configuration error: {message}")


class FileProcessingError(RepopackError):
"""Raised when there's an error processing a file."""

pass
def __init__(self, file_path: str, error_message: str) -> None:
"""
Initialize the FileProcessingError.
Args:
file_path (str): The path of the file that caused the error.
error_message (str): The specific error message.
"""
super().__init__(f"Error processing file '{file_path}': {error_message}")


class OutputGenerationError(RepopackError):
"""Raised when there's an error generating the output."""

pass
def __init__(self, error_message: str) -> None:
"""
Initialize the OutputGenerationError.
Args:
error_message (str): The specific error message related to output generation.
"""
super().__init__(f"Error generating output: {error_message}")
93 changes: 74 additions & 19 deletions repopack/output_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,22 @@ def generate_output(
sanitized_files: List[Dict[str, str]],
all_file_paths: List[str],
file_char_counts: Dict[str, int],
):
output_path = os.path.join(root_dir, config["output"]["file_path"])
tree_string = generate_tree_string(all_file_paths)
) -> None:
"""
Generate the output file based on the specified configuration.
Args:
root_dir (str): The root directory of the repository.
config (Dict[str, Any]): The configuration dictionary.
sanitized_files (List[Dict[str, str]]): List of sanitized file contents.
all_file_paths (List[str]): List of all file paths in the repository.
file_char_counts (Dict[str, int]): Dictionary of file paths and their character counts.
Raises:
OutputGenerationError: If there's an error during output generation.
"""
output_path: str = os.path.join(root_dir, config["output"]["file_path"])
tree_string: str = generate_tree_string(all_file_paths)

try:
if config["output"]["style"] == "xml":
Expand All @@ -37,20 +50,40 @@ def generate_plain_output(
all_file_paths: List[str],
file_char_counts: Dict[str, int],
tree_string: str,
):
) -> None:
"""
Generate plain text output file.
Args:
output_path (str): Path to the output file.
config (Dict[str, Any]): The configuration dictionary.
sanitized_files (List[Dict[str, str]]): List of sanitized file contents.
all_file_paths (List[str]): List of all file paths in the repository.
file_char_counts (Dict[str, int]): Dictionary of file paths and their character counts.
tree_string (str): String representation of the repository structure.
"""
with open(output_path, "w", encoding="utf-8") as f:
# Write header
f.write("=" * 64 + "\n")
f.write("Repopack Output File\n")
f.write("=" * 64 + "\n\n")
f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n")

# Write purpose
f.write("Purpose:\n--------\n")
f.write("This file contains a packed representation of the entire repository's contents.\n")
f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n")
f.write("or other automated processes.\n\n")

# Write notes
if config["output"]["show_line_numbers"]:
f.write("- Line numbers have been added to the beginning of each line.\n")

# Write repository structure
f.write("Repository Structure:\n---------------------\n")
f.write(tree_string + "\n\n")

# Write repository files
f.write("=" * 64 + "\n")
f.write("Repository Files\n")
f.write("=" * 64 + "\n\n")
Expand All @@ -61,13 +94,16 @@ def generate_plain_output(
f.write("=" * 16 + "\n")
f.write(file["content"] + "\n\n")

top_files_length = config["output"]["top_files_length"]
# Write top files by character count
top_files_length: int = config["output"]["top_files_length"]
if top_files_length > 0:
f.write("\n" + "=" * 64 + "\n")
f.write(f"Top {top_files_length} Files by Character Count\n")
f.write("=" * 64 + "\n")

sorted_files = sorted(file_char_counts.items(), key=lambda x: x[1], reverse=True)
sorted_files: List[tuple[str, int]] = sorted(
file_char_counts.items(), key=lambda x: x[1], reverse=True
)
for i, (file_path, char_count) in enumerate(sorted_files[:top_files_length], 1):
f.write(f"{i}. {file_path} ({char_count} chars)\n")

Expand All @@ -81,10 +117,22 @@ def generate_xml_output(
all_file_paths: List[str],
file_char_counts: Dict[str, int],
tree_string: str,
):
root = ET.Element("repopack_output")

summary = ET.SubElement(root, "summary")
) -> None:
"""
Generate XML output file.
Args:
output_path (str): Path to the output file.
config (Dict[str, Any]): The configuration dictionary.
sanitized_files (List[Dict[str, str]]): List of sanitized file contents.
all_file_paths (List[str]): List of all file paths in the repository.
file_char_counts (Dict[str, int]): Dictionary of file paths and their character counts.
tree_string (str): String representation of the repository structure.
"""
root: ET.Element = ET.Element("repopack_output")

# Add summary section
summary: ET.Element = ET.SubElement(root, "summary")
ET.SubElement(summary, "header").text = (
f"Repopack Output File"
"\nThis file was generated by Repopack on: {datetime.now().isoformat()}"
Expand All @@ -95,36 +143,43 @@ def generate_xml_output(
"or other automated processes."
)

notes = ET.SubElement(summary, "notes")
# Add notes
notes: ET.Element = ET.SubElement(summary, "notes")
if config["output"]["remove_comments"]:
ET.SubElement(notes, "note").text = "Code comments have been removed."
if config["output"]["show_line_numbers"]:
ET.SubElement(
notes, "note"
).text = "Line numbers have been added to the beginning of each line."

# Add repository structure
ET.SubElement(root, "repository_structure").text = tree_string

files = ET.SubElement(root, "repository_files")
# Add repository files
files: ET.Element = ET.SubElement(root, "repository_files")
for file in sanitized_files:
file_elem = ET.SubElement(files, "file")
file_elem: ET.Element = ET.SubElement(files, "file")
file_elem.set("path", file["path"])
file_elem.text = file["content"]

top_files_length = config["output"]["top_files_length"]
# Add top files by character count
top_files_length: int = config["output"]["top_files_length"]
if top_files_length > 0:
top_files = ET.SubElement(root, "top_files")
top_files: ET.Element = ET.SubElement(root, "top_files")
top_files.set("count", str(top_files_length))
sorted_files = sorted(file_char_counts.items(), key=lambda x: x[1], reverse=True)
sorted_files: List[tuple[str, int]] = sorted(
file_char_counts.items(), key=lambda x: x[1], reverse=True
)
for i, (file_path, char_count) in enumerate(sorted_files[:top_files_length], 1):
file_elem = ET.SubElement(top_files, "file")
file_elem: ET.Element = ET.SubElement(top_files, "file")
file_elem.set("rank", str(i))
file_elem.set("path", file_path)
file_elem.set("char_count", str(char_count))

# Pretty print the XML
xml_string = ET.tostring(root, encoding="unicode")
pretty_xml = minidom.parseString(xml_string).toprettyxml(indent=" ")
xml_string: str = ET.tostring(root, encoding="unicode")
pretty_xml: str = minidom.parseString(xml_string).toprettyxml(indent=" ")

# Write to file
with open(output_path, "w", encoding="utf-8") as f:
f.write(pretty_xml)
Loading

0 comments on commit 8c45cdd

Please sign in to comment.