9
9
10
10
import requests .exceptions
11
11
import tiktoken
12
- from jinja2 import Environment , FileSystemLoader , TemplateNotFound
12
+ from jinja2 import Environment , FileSystemLoader , Template , TemplateNotFound
13
13
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
16
16
from gitingest .utils .compat_func import readlink
17
17
from gitingest .utils .logging_config import get_logger
18
18
28
28
]
29
29
30
30
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
-
74
31
def _gather_file_contents (node : FileSystemNode ) -> str :
75
32
"""Recursively gather contents of all files under the given node.
76
33
@@ -181,71 +138,76 @@ def _format_token_count(text: str) -> str | None:
181
138
182
139
def generate_digest (context : ContextV1 ) -> str :
183
140
"""Generate a digest string from a ContextV1 object.
184
-
141
+
185
142
This is a convenience function that uses the DefaultFormatter to format a ContextV1.
186
-
143
+
187
144
Parameters
188
145
----------
189
146
context : ContextV1
190
147
The ContextV1 object containing sources and query information.
191
-
148
+
192
149
Returns
193
150
-------
194
151
str
195
152
The formatted digest string.
153
+
196
154
"""
197
155
formatter = DefaultFormatter ()
198
156
return formatter .format (context , context .query )
199
157
200
158
201
159
class DefaultFormatter :
202
- def __init__ (self ):
160
+ """Default formatter for rendering filesystem nodes using Jinja2 templates."""
161
+
162
+ def __init__ (self ) -> None :
203
163
self .separator = SEPARATOR
204
164
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 :
208
168
"""Get template based on node class name."""
209
169
template_name = f"{ node .__class__ .__name__ } .j2"
210
170
return self .env .get_template (template_name )
211
171
212
172
@singledispatchmethod
213
- def format (self , node : Source , query ) :
173
+ def format (self , node : Source , query : IngestionQuery ) -> str :
214
174
"""Dynamically format any node type based on available templates."""
215
175
try :
216
176
template = self ._get_template_for_node (node )
217
177
# Provide common template variables
218
178
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 ,
223
183
}
224
184
# Special handling for ContextV1 objects
225
185
if isinstance (node , ContextV1 ):
226
- context_vars [' context' ] = node
186
+ context_vars [" context" ] = node
227
187
# Use ContextV1 for backward compatibility
228
188
template = self .env .get_template ("ContextV1.j2" )
229
-
189
+
230
190
return template .render (** context_vars )
231
191
except TemplateNotFound :
232
192
# Fallback: return content if available, otherwise empty string
233
193
return f"{ getattr (node , 'content' , '' )} "
234
194
235
195
236
196
class DebugFormatter :
237
- def __init__ (self ):
197
+ """Debug formatter that shows detailed information about filesystem nodes."""
198
+
199
+ def __init__ (self ) -> None :
238
200
self .separator = SEPARATOR
239
201
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 :
243
205
"""Get template based on node class name."""
244
206
template_name = f"{ node .__class__ .__name__ } .j2"
245
207
return self .env .get_template (template_name )
246
208
247
209
@singledispatchmethod
248
- def format (self , node : Source , query ) :
210
+ def format (self , node : Source , query : IngestionQuery ) -> str :
249
211
"""Dynamically format any node type with debug information."""
250
212
try :
251
213
# Get the actual class name
@@ -255,11 +217,15 @@ def format(self, node: Source, query):
255
217
field_names = []
256
218
257
219
# Try to get dataclass fields first
220
+ def _raise_no_dataclass_fields () -> None :
221
+ msg = "No dataclass fields found"
222
+ raise AttributeError (msg )
223
+
258
224
try :
259
225
if hasattr (node , "__dataclass_fields__" ) and hasattr (node .__dataclass_fields__ , "keys" ):
260
226
field_names .extend (node .__dataclass_fields__ .keys ())
261
227
else :
262
- raise AttributeError # Fall through to backup method
228
+ _raise_no_dataclass_fields () # Fall through to backup method
263
229
except (AttributeError , TypeError ):
264
230
# Fall back to getting all non-private attributes
265
231
field_names = [
@@ -268,20 +234,20 @@ def format(self, node: Source, query):
268
234
269
235
# Format the debug output
270
236
fields_str = ", " .join (field_names )
271
-
237
+
272
238
# Try to get specific template, fallback to Source.j2
273
239
try :
274
240
template = self ._get_template_for_node (node )
275
241
except TemplateNotFound :
276
242
template = self .env .get_template ("Source.j2" )
277
-
243
+
278
244
return template .render (
279
245
SEPARATOR = SEPARATOR ,
280
246
class_name = class_name ,
281
247
fields_str = fields_str ,
282
248
node = node ,
283
249
query = query ,
284
- formatter = self
250
+ formatter = self ,
285
251
)
286
252
except TemplateNotFound :
287
253
# Ultimate fallback
@@ -291,34 +257,34 @@ def format(self, node: Source, query):
291
257
class SummaryFormatter :
292
258
"""Dedicated formatter for generating summaries of filesystem nodes."""
293
259
294
- def __init__ (self ):
260
+ def __init__ (self ) -> None :
295
261
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 :
299
265
"""Get template based on node class name."""
300
266
template_name = f"{ node .__class__ .__name__ } .j2"
301
267
return self .env .get_template (template_name )
302
268
303
269
@singledispatchmethod
304
- def summary (self , node : Source , query ) :
270
+ def summary (self , node : Source , query : IngestionQuery ) -> str :
305
271
"""Dynamically generate summary for any node type based on available templates."""
306
272
try :
307
273
# Provide common template variables
308
274
context_vars = {
309
- ' node' : node ,
310
- ' query' : query ,
311
- ' formatter' : self
275
+ " node" : node ,
276
+ " query" : query ,
277
+ " formatter" : self ,
312
278
}
313
-
279
+
314
280
# Special handling for ContextV1 objects
315
281
if isinstance (node , ContextV1 ):
316
- context_vars [' context' ] = node
282
+ context_vars [" context" ] = node
317
283
# Use ContextV1 for backward compatibility
318
284
template = self .env .get_template ("ContextV1.j2" )
319
285
else :
320
286
template = self ._get_template_for_node (node )
321
-
287
+
322
288
return template .render (** context_vars )
323
289
except TemplateNotFound :
324
290
# Fallback: return name if available
0 commit comments