Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d63057e

Browse files
feat: move contextv1 to its own file
1 parent 83fde0f commit d63057e

File tree

8 files changed

+159
-121
lines changed

8 files changed

+159
-121
lines changed

‎.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ repos:
8484
rev: v1.36.4
8585
hooks:
8686
- id: djlint-reformat-jinja
87+
exclude: ^src/gitingest/format/
8788

8889
- repo: https://github.com/igorshubovych/markdownlint-cli
8990
rev: v0.45.0
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Generated using https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}
1+
# Generated using https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}{{ context.query.subpath }}
22

33
Sources used:
4-
{%- for source in context.sources %}
4+
{%- for source in context %}
55
- {{ source.name }}: {{ source.__class__.__name__ }}
66
{% endfor %}
77

88
{%- for source in context.sources %}
99
{{ formatter.format(source, context.query) }}
1010
{%- endfor %}
11-
# End of generated content
11+
# End of https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}{{ context.query.subpath }}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Repository: {{ context.query.user_name }}/{{ context.query.repo_name }}
22
Commit: {{ context.query.commit }}
3-
Files analyzed: {{ context.sources[0].file_count }}
3+
Files analyzed: {{ context.file_count }}

‎src/gitingest/output_formatter.py

Lines changed: 46 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import requests.exceptions
1111
import tiktoken
12-
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
12+
from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
1313

14-
from gitingest.schemas import FileSystemDirectory, FileSystemFile, FileSystemNode, FileSystemSymlink, Source
15-
from gitingest.schemas.filesystem import SEPARATOR, ContextV1, FileSystemNodeType, GitRepository
14+
from gitingest.schemas import ContextV1, FileSystemNode, Source
15+
from gitingest.schemas.filesystem import SEPARATOR, FileSystemNodeType
1616
from gitingest.utils.compat_func import readlink
1717
from gitingest.utils.logging_config import get_logger
1818

@@ -28,49 +28,6 @@
2828
]
2929

3030

31-
# Backward compatibility
32-
33-
34-
def _create_summary_prefix(query: IngestionQuery, *, single_file: bool = False) -> str:
35-
"""Create a prefix string for summarizing a repository or local directory.
36-
37-
Includes repository name (if provided), commit/branch details, and subpath if relevant.
38-
39-
Parameters
40-
----------
41-
query : IngestionQuery
42-
The parsed query object containing information about the repository and query parameters.
43-
single_file : bool
44-
A flag indicating whether the summary is for a single file (default: ``False``).
45-
46-
Returns
47-
-------
48-
str
49-
A summary prefix string containing repository, commit, branch, and subpath details.
50-
51-
"""
52-
parts = []
53-
54-
if query.user_name:
55-
parts.append(f"Repository: {query.user_name}/{query.repo_name}")
56-
else:
57-
# Local scenario
58-
parts.append(f"Directory: {query.slug}")
59-
60-
if query.tag:
61-
parts.append(f"Tag: {query.tag}")
62-
elif query.branch and query.branch not in ("main", "master"):
63-
parts.append(f"Branch: {query.branch}")
64-
65-
if query.commit:
66-
parts.append(f"Commit: {query.commit}")
67-
68-
if query.subpath != "/" and not single_file:
69-
parts.append(f"Subpath: {query.subpath}")
70-
71-
return "\n".join(parts) + "\n"
72-
73-
7431
def _gather_file_contents(node: FileSystemNode) -> str:
7532
"""Recursively gather contents of all files under the given node.
7633
@@ -181,71 +138,76 @@ def _format_token_count(text: str) -> str | None:
181138

182139
def generate_digest(context: ContextV1) -> str:
183140
"""Generate a digest string from a ContextV1 object.
184-
141+
185142
This is a convenience function that uses the DefaultFormatter to format a ContextV1.
186-
143+
187144
Parameters
188145
----------
189146
context : ContextV1
190147
The ContextV1 object containing sources and query information.
191-
148+
192149
Returns
193150
-------
194151
str
195152
The formatted digest string.
153+
196154
"""
197155
formatter = DefaultFormatter()
198156
return formatter.format(context, context.query)
199157

200158

201159
class DefaultFormatter:
202-
def __init__(self):
160+
"""Default formatter for rendering filesystem nodes using Jinja2 templates."""
161+
162+
def __init__(self) -> None:
203163
self.separator = SEPARATOR
204164
template_dir = Path(__file__).parent / "format" / "DefaultFormatter"
205-
self.env = Environment(loader=FileSystemLoader(template_dir))
206-
207-
def _get_template_for_node(self, node):
165+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
166+
167+
def _get_template_for_node(self, node: Source) ->Template:
208168
"""Get template based on node class name."""
209169
template_name = f"{node.__class__.__name__}.j2"
210170
return self.env.get_template(template_name)
211171

212172
@singledispatchmethod
213-
def format(self, node: Source, query):
173+
def format(self, node: Source, query: IngestionQuery) ->str:
214174
"""Dynamically format any node type based on available templates."""
215175
try:
216176
template = self._get_template_for_node(node)
217177
# Provide common template variables
218178
context_vars = {
219-
'node': node,
220-
'query': query,
221-
'formatter': self,
222-
'SEPARATOR': SEPARATOR
179+
"node": node,
180+
"query": query,
181+
"formatter": self,
182+
"SEPARATOR": SEPARATOR,
223183
}
224184
# Special handling for ContextV1 objects
225185
if isinstance(node, ContextV1):
226-
context_vars['context'] = node
186+
context_vars["context"] = node
227187
# Use ContextV1 for backward compatibility
228188
template = self.env.get_template("ContextV1.j2")
229-
189+
230190
return template.render(**context_vars)
231191
except TemplateNotFound:
232192
# Fallback: return content if available, otherwise empty string
233193
return f"{getattr(node, 'content', '')}"
234194

235195

236196
class DebugFormatter:
237-
def __init__(self):
197+
"""Debug formatter that shows detailed information about filesystem nodes."""
198+
199+
def __init__(self) -> None:
238200
self.separator = SEPARATOR
239201
template_dir = Path(__file__).parent / "format" / "DebugFormatter"
240-
self.env = Environment(loader=FileSystemLoader(template_dir))
241-
242-
def _get_template_for_node(self, node):
202+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
203+
204+
def _get_template_for_node(self, node: Source) ->Template:
243205
"""Get template based on node class name."""
244206
template_name = f"{node.__class__.__name__}.j2"
245207
return self.env.get_template(template_name)
246208

247209
@singledispatchmethod
248-
def format(self, node: Source, query):
210+
def format(self, node: Source, query: IngestionQuery) ->str:
249211
"""Dynamically format any node type with debug information."""
250212
try:
251213
# Get the actual class name
@@ -255,11 +217,15 @@ def format(self, node: Source, query):
255217
field_names = []
256218

257219
# Try to get dataclass fields first
220+
def _raise_no_dataclass_fields() -> None:
221+
msg = "No dataclass fields found"
222+
raise AttributeError(msg)
223+
258224
try:
259225
if hasattr(node, "__dataclass_fields__") and hasattr(node.__dataclass_fields__, "keys"):
260226
field_names.extend(node.__dataclass_fields__.keys())
261227
else:
262-
raiseAttributeError # Fall through to backup method
228+
_raise_no_dataclass_fields() # Fall through to backup method
263229
except (AttributeError, TypeError):
264230
# Fall back to getting all non-private attributes
265231
field_names = [
@@ -268,20 +234,20 @@ def format(self, node: Source, query):
268234

269235
# Format the debug output
270236
fields_str = ", ".join(field_names)
271-
237+
272238
# Try to get specific template, fallback to Source.j2
273239
try:
274240
template = self._get_template_for_node(node)
275241
except TemplateNotFound:
276242
template = self.env.get_template("Source.j2")
277-
243+
278244
return template.render(
279245
SEPARATOR=SEPARATOR,
280246
class_name=class_name,
281247
fields_str=fields_str,
282248
node=node,
283249
query=query,
284-
formatter=self
250+
formatter=self,
285251
)
286252
except TemplateNotFound:
287253
# Ultimate fallback
@@ -291,34 +257,34 @@ def format(self, node: Source, query):
291257
class SummaryFormatter:
292258
"""Dedicated formatter for generating summaries of filesystem nodes."""
293259

294-
def __init__(self):
260+
def __init__(self)->None:
295261
template_dir = Path(__file__).parent / "format" / "SummaryFormatter"
296-
self.env = Environment(loader=FileSystemLoader(template_dir))
297-
298-
def _get_template_for_node(self, node):
262+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
263+
264+
def _get_template_for_node(self, node: Source) ->Template:
299265
"""Get template based on node class name."""
300266
template_name = f"{node.__class__.__name__}.j2"
301267
return self.env.get_template(template_name)
302268

303269
@singledispatchmethod
304-
def summary(self, node: Source, query):
270+
def summary(self, node: Source, query: IngestionQuery) ->str:
305271
"""Dynamically generate summary for any node type based on available templates."""
306272
try:
307273
# Provide common template variables
308274
context_vars = {
309-
'node': node,
310-
'query': query,
311-
'formatter': self
275+
"node": node,
276+
"query": query,
277+
"formatter": self,
312278
}
313-
279+
314280
# Special handling for ContextV1 objects
315281
if isinstance(node, ContextV1):
316-
context_vars['context'] = node
282+
context_vars["context"] = node
317283
# Use ContextV1 for backward compatibility
318284
template = self.env.get_template("ContextV1.j2")
319285
else:
320286
template = self._get_template_for_node(node)
321-
287+
322288
return template.render(**context_vars)
323289
except TemplateNotFound:
324290
# Fallback: return name if available

‎src/gitingest/schemas/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Module containing the schemas for the Gitingest package."""
22

33
from gitingest.schemas.cloning import CloneConfig
4+
from gitingest.schemas.contextv1 import ContextV1
45
from gitingest.schemas.filesystem import (
5-
ContextV1,
66
FileSystemDirectory,
77
FileSystemFile,
88
FileSystemNode,
@@ -23,4 +23,5 @@
2323
"FileSystemSymlink",
2424
"GitRepository",
2525
"IngestionQuery",
26+
"Source",
2627
]

‎src/gitingest/schemas/contextv1.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Schema for ContextV1 objects used in formatting."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import TYPE_CHECKING, Iterator
7+
8+
from gitingest.schemas.filesystem import FileSystemDirectory, FileSystemNode, Source
9+
10+
if TYPE_CHECKING:
11+
from gitingest.schemas import IngestionQuery
12+
13+
14+
@dataclass
15+
class ContextV1:
16+
"""The ContextV1 object is an object that contains all information needed to produce a formatted output.
17+
18+
This object contains all information needed to produce a formatted output
19+
similar to the "legacy" output.
20+
21+
Attributes
22+
----------
23+
sources : list[Source]
24+
List of source objects (files, directories, etc.)
25+
query : IngestionQuery
26+
The query context.
27+
28+
"""
29+
30+
sources: list[Source]
31+
query: IngestionQuery
32+
33+
@property
34+
def sources_by_type(self) -> dict[str, list[Source]]:
35+
"""Return sources grouped by their class name."""
36+
result = {}
37+
for source in self.sources:
38+
class_name = source.__class__.__name__
39+
if class_name not in result:
40+
result[class_name] = []
41+
result[class_name].append(source)
42+
return result
43+
44+
def __getitem__(self, key: str) -> list[Source]:
45+
"""Allow dict-like access to sources by type name."""
46+
sources_dict = self.sources_by_type
47+
if key not in sources_dict:
48+
error_msg = f"No sources of type '{key}' found"
49+
raise KeyError(error_msg)
50+
return sources_dict[key]
51+
52+
def __iter__(self) -> Iterator[Source]:
53+
"""Allow iteration over all sources."""
54+
return iter(self.sources)
55+
56+
@property
57+
def file_count(self) -> int:
58+
"""Calculate total file count based on sources."""
59+
# No need to iterate on children, directories are already aware of their
60+
# file count
61+
total = 0
62+
for source in self.sources:
63+
if isinstance(source, FileSystemDirectory):
64+
# For directories, add their file_count
65+
total += source.file_count
66+
elif isinstance(source, FileSystemNode):
67+
# For individual files/nodes, increment by 1
68+
total += 1
69+
return total

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /