Skip to content

Commit b5ffa5f

Browse files
filipchristiansenRyanL2004
authored andcommitted
Refactor CLI to wrap async logic with a sync command function (#136)
- Change main() to a synchronous Click command - Introduce _async_main() for async ingest logic - Use asyncio.run(...) to properly await the async function
1 parent d721b00 commit b5ffa5f

File tree

3 files changed

+182
-6
lines changed

3 files changed

+182
-6
lines changed

src/gitingest/cli.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,51 @@
22

33
# pylint: disable=no-value-for-parameter
44

5+
import asyncio
6+
57
import click
68

79
from config import MAX_FILE_SIZE
810
from gitingest.repository_ingest import ingest
911

1012

1113
@click.command()
12-
@click.argument("source", type=str, required=True)
14+
@click.argument("source", type=str, default=".")
1315
@click.option("--output", "-o", default=None, help="Output file path (default: <repo_name>.txt in current directory)")
1416
@click.option("--max-size", "-s", default=MAX_FILE_SIZE, help="Maximum file size to process in bytes")
1517
@click.option("--exclude-pattern", "-e", multiple=True, help="Patterns to exclude")
1618
@click.option("--include-pattern", "-i", multiple=True, help="Patterns to include")
17-
async def main(
19+
def main(
20+
source: str,
21+
output: str | None,
22+
max_size: int,
23+
exclude_pattern: tuple[str, ...],
24+
include_pattern: tuple[str, ...],
25+
):
26+
"""
27+
Main entry point for the CLI. This function is called when the CLI is run as a script.
28+
29+
It calls the async main function to run the command.
30+
31+
Parameters
32+
----------
33+
source : str
34+
The source directory or repository to analyze.
35+
output : str | None
36+
The path where the output file will be written. If not specified, the output will be written
37+
to a file named `<repo_name>.txt` in the current directory.
38+
max_size : int
39+
The maximum file size to process, in bytes. Files larger than this size will be ignored.
40+
exclude_pattern : tuple[str, ...]
41+
A tuple of patterns to exclude during the analysis. Files matching these patterns will be ignored.
42+
include_pattern : tuple[str, ...]
43+
A tuple of patterns to include during the analysis. Only files matching these patterns will be processed.
44+
"""
45+
# Main entry point for the CLI. This function is called when the CLI is run as a script.
46+
asyncio.run(_async_main(source, output, max_size, exclude_pattern, include_pattern))
47+
48+
49+
async def _async_main(
1850
source: str,
1951
output: str | None,
2052
max_size: int,
@@ -24,9 +56,8 @@ async def main(
2456
"""
2557
Analyze a directory or repository and create a text dump of its contents.
2658
27-
This command analyzes the contents of a specified source directory or repository,
28-
applies custom include and exclude patterns, and generates a text summary of the analysis
29-
which is then written to an output file.
59+
This command analyzes the contents of a specified source directory or repository, applies custom include and
60+
exclude patterns, and generates a text summary of the analysis which is then written to an output file.
3061
3162
Parameters
3263
----------

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def rate_limit_exception_handler(request: Request, exc: Exception) -> Resp
175175
app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
176176

177177
# Set up template rendering
178-
templates = Jinja2Templates(directory="templates")
178+
templates = Jinja2Templates(directory="src/templates")
179179

180180

181181
@app.get("/health")

tests/test_flow_integration.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Integration tests for GitIngest.
3+
These tests cover core functionalities, edge cases, and concurrency handling.
4+
"""
5+
6+
import shutil
7+
from concurrent.futures import ThreadPoolExecutor
8+
from pathlib import Path
9+
from unittest.mock import patch
10+
11+
import pytest
12+
from fastapi.testclient import TestClient
13+
14+
from main import app
15+
16+
BASE_DIR = Path(__file__).resolve().parent.parent
17+
TEMPLATE_DIR = BASE_DIR / "src" / "templates"
18+
19+
20+
@pytest.fixture(scope="module")
21+
def test_client():
22+
"""Create a test client fixture."""
23+
with TestClient(app) as client:
24+
client.headers.update({"Host": "localhost"})
25+
yield client
26+
27+
28+
@pytest.fixture(scope="module", autouse=True)
29+
def mock_templates():
30+
"""Mock Jinja2 template rendering to bypass actual file loading."""
31+
with patch("starlette.templating.Jinja2Templates.TemplateResponse") as mock_template:
32+
mock_template.return_value = "Mocked Template Response"
33+
yield mock_template
34+
35+
36+
def cleanup_temp_directories():
37+
temp_dir = Path("/tmp/gitingest")
38+
if temp_dir.exists():
39+
try:
40+
shutil.rmtree(temp_dir)
41+
except PermissionError as e:
42+
print(f"Error cleaning up {temp_dir}: {e}")
43+
44+
45+
@pytest.fixture(scope="module", autouse=True)
46+
def cleanup():
47+
"""Cleanup temporary directories after tests."""
48+
yield
49+
cleanup_temp_directories()
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_remote_repository_analysis(test_client): # pylint: disable=redefined-outer-name
54+
"""Test the complete flow of analyzing a remote repository."""
55+
form_data = {
56+
"input_text": "https://github.com/octocat/Hello-World",
57+
"max_file_size": "243",
58+
"pattern_type": "exclude",
59+
"pattern": "",
60+
}
61+
62+
response = test_client.post("/", data=form_data)
63+
assert response.status_code == 200, f"Form submission failed: {response.text}"
64+
assert "Mocked Template Response" in response.text
65+
66+
67+
@pytest.mark.asyncio
68+
async def test_invalid_repository_url(test_client): # pylint: disable=redefined-outer-name
69+
"""Test handling of an invalid repository URL."""
70+
form_data = {
71+
"input_text": "https://github.com/nonexistent/repo",
72+
"max_file_size": "243",
73+
"pattern_type": "exclude",
74+
"pattern": "",
75+
}
76+
77+
response = test_client.post("/", data=form_data)
78+
assert response.status_code == 200, f"Request failed: {response.text}"
79+
assert "Mocked Template Response" in response.text
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_large_repository(test_client): # pylint: disable=redefined-outer-name
84+
"""Simulate analysis of a large repository with nested folders."""
85+
form_data = {
86+
"input_text": "https://github.com/large/repo-with-many-files",
87+
"max_file_size": "243",
88+
"pattern_type": "exclude",
89+
"pattern": "",
90+
}
91+
92+
response = test_client.post("/", data=form_data)
93+
assert response.status_code == 200, f"Request failed: {response.text}"
94+
assert "Mocked Template Response" in response.text
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_concurrent_requests(test_client): # pylint: disable=redefined-outer-name
99+
"""Test handling of multiple concurrent requests."""
100+
101+
def make_request():
102+
form_data = {
103+
"input_text": "https://github.com/octocat/Hello-World",
104+
"max_file_size": "243",
105+
"pattern_type": "exclude",
106+
"pattern": "",
107+
}
108+
response = test_client.post("/", data=form_data)
109+
assert response.status_code == 200, f"Request failed: {response.text}"
110+
assert "Mocked Template Response" in response.text
111+
112+
with ThreadPoolExecutor(max_workers=5) as executor:
113+
futures = [executor.submit(make_request) for _ in range(5)]
114+
for future in futures:
115+
future.result()
116+
117+
118+
@pytest.mark.asyncio
119+
async def test_large_file_handling(test_client): # pylint: disable=redefined-outer-name
120+
"""Test handling of repositories with large files."""
121+
form_data = {
122+
"input_text": "https://github.com/octocat/Hello-World",
123+
"max_file_size": "1",
124+
"pattern_type": "exclude",
125+
"pattern": "",
126+
}
127+
128+
response = test_client.post("/", data=form_data)
129+
assert response.status_code == 200, f"Request failed: {response.text}"
130+
assert "Mocked Template Response" in response.text
131+
132+
133+
@pytest.mark.asyncio
134+
async def test_repository_with_patterns(test_client): # pylint: disable=redefined-outer-name
135+
"""Test repository analysis with include/exclude patterns."""
136+
form_data = {
137+
"input_text": "https://github.com/octocat/Hello-World",
138+
"max_file_size": "243",
139+
"pattern_type": "include",
140+
"pattern": "*.md",
141+
}
142+
143+
response = test_client.post("/", data=form_data)
144+
assert response.status_code == 200, f"Request failed: {response.text}"
145+
assert "Mocked Template Response" in response.text

0 commit comments

Comments
 (0)