Navegador

navegador / tests / test_python_parser.py
Blame History Raw 419 lines
1
"""Tests for navegador.ingestion.python — PythonParser internal methods."""
2
3
from unittest.mock import MagicMock, patch
4
5
import pytest
6
7
from navegador.graph.schema import NodeLabel
8
9
# ── Mock tree-sitter node ──────────────────────────────────────────────────────
10
11
class MockNode:
12
"""Minimal mock of a tree-sitter Node."""
13
def __init__(self, type_: str, text: bytes = b"", children: list = None,
14
start_byte: int = 0, end_byte: int = 0,
15
start_point: tuple = (0, 0), end_point: tuple = (0, 0)):
16
self.type = type_
17
self._text = text
18
self.children = children or []
19
self.start_byte = start_byte
20
self.end_byte = end_byte
21
self.start_point = start_point
22
self.end_point = end_point
23
24
25
def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
26
return MockNode(type_, text, start_byte=0, end_byte=len(text))
27
28
29
def _make_store():
30
store = MagicMock()
31
store.query.return_value = MagicMock(result_set=[])
32
return store
33
34
35
# ── _node_text ────────────────────────────────────────────────────────────────
36
37
class TestNodeText:
38
def test_extracts_text_from_source(self):
39
from navegador.ingestion.python import _node_text
40
source = b"hello world"
41
node = MockNode("identifier", start_byte=6, end_byte=11)
42
assert _node_text(node, source) == "world"
43
44
def test_full_source(self):
45
from navegador.ingestion.python import _node_text
46
source = b"foo_bar"
47
node = MockNode("identifier", start_byte=0, end_byte=7)
48
assert _node_text(node, source) == "foo_bar"
49
50
def test_handles_utf8(self):
51
from navegador.ingestion.python import _node_text
52
source = "héllo".encode("utf-8")
53
node = MockNode("identifier", start_byte=0, end_byte=len(source))
54
assert "llo" in _node_text(node, source)
55
56
57
# ── _get_docstring ────────────────────────────────────────────────────────────
58
59
class TestGetDocstring:
60
def test_returns_none_when_no_block(self):
61
from navegador.ingestion.python import _get_docstring
62
node = MockNode("function_definition", children=[
63
MockNode("identifier")
64
])
65
assert _get_docstring(node, b"def foo(): pass") is None
66
67
def test_returns_none_when_no_expression_stmt(self):
68
from navegador.ingestion.python import _get_docstring
69
block = MockNode("block", children=[
70
MockNode("return_statement")
71
])
72
fn = MockNode("function_definition", children=[block])
73
assert _get_docstring(fn, b"") is None
74
75
def test_returns_none_when_no_string_in_expr(self):
76
from navegador.ingestion.python import _get_docstring
77
expr_stmt = MockNode("expression_statement", children=[
78
MockNode("assignment")
79
])
80
block = MockNode("block", children=[expr_stmt])
81
fn = MockNode("function_definition", children=[block])
82
assert _get_docstring(fn, b"") is None
83
84
def test_extracts_docstring(self):
85
from navegador.ingestion.python import _get_docstring
86
source = b'"""My docstring."""'
87
string_node = MockNode("string", start_byte=0, end_byte=len(source))
88
expr_stmt = MockNode("expression_statement", children=[string_node])
89
block = MockNode("block", children=[expr_stmt])
90
fn = MockNode("function_definition", children=[block])
91
result = _get_docstring(fn, source)
92
assert "My docstring." in result
93
94
95
# ── _get_python_language error ─────────────────────────────────────────────────
96
97
class TestGetPythonLanguage:
98
def test_raises_import_error_when_not_installed(self):
99
from navegador.ingestion.python import _get_python_language
100
with patch.dict("sys.modules", {"tree_sitter_python": None, "tree_sitter": None}):
101
with pytest.raises(ImportError, match="tree-sitter-python"):
102
_get_python_language()
103
104
105
# ── PythonParser with mocked parser ──────────────────────────────────────────
106
107
class TestPythonParserHandlers:
108
def _make_parser(self):
109
"""Create PythonParser bypassing tree-sitter init."""
110
from navegador.ingestion.python import PythonParser
111
with patch("navegador.ingestion.python._get_parser") as mock_get:
112
mock_get.return_value = MagicMock()
113
parser = PythonParser()
114
return parser
115
116
def test_handle_import(self):
117
parser = self._make_parser()
118
store = _make_store()
119
source = b"import os.path"
120
121
dotted = _text_node(b"os.path", "dotted_name")
122
import_node = MockNode("import_statement", children=[dotted],
123
start_point=(0, 0))
124
stats = {"functions": 0, "classes": 0, "edges": 0}
125
parser._handle_import(import_node, source, "app.py", store, stats)
126
store.create_node.assert_called_once()
127
store.create_edge.assert_called_once()
128
assert stats["edges"] == 1
129
130
def test_handle_import_no_dotted_name(self):
131
parser = self._make_parser()
132
store = _make_store()
133
import_node = MockNode("import_statement", children=[
134
MockNode("keyword", b"import")
135
], start_point=(0, 0))
136
stats = {"functions": 0, "classes": 0, "edges": 0}
137
parser._handle_import(import_node, b"import x", "app.py", store, stats)
138
store.create_node.assert_not_called()
139
140
def test_handle_class(self):
141
parser = self._make_parser()
142
store = _make_store()
143
source = b"class MyClass: pass"
144
name_node = _text_node(b"MyClass")
145
class_node = MockNode("class_definition",
146
children=[name_node],
147
start_point=(0, 0), end_point=(0, 18))
148
stats = {"functions": 0, "classes": 0, "edges": 0}
149
parser._handle_class(class_node, source, "app.py", store, stats)
150
assert stats["classes"] == 1
151
assert stats["edges"] == 1
152
store.create_node.assert_called()
153
154
def test_handle_class_no_identifier(self):
155
parser = self._make_parser()
156
store = _make_store()
157
class_node = MockNode("class_definition", children=[
158
MockNode("keyword", b"class")
159
], start_point=(0, 0), end_point=(0, 0))
160
stats = {"functions": 0, "classes": 0, "edges": 0}
161
parser._handle_class(class_node, b"class: pass", "app.py", store, stats)
162
assert stats["classes"] == 0
163
164
def test_handle_class_with_inheritance(self):
165
parser = self._make_parser()
166
store = _make_store()
167
source = b"class Child(Parent): pass"
168
name_node = _text_node(b"Child")
169
parent_id = _text_node(b"Parent")
170
arg_list = MockNode("argument_list", children=[parent_id])
171
class_node = MockNode("class_definition",
172
children=[name_node, arg_list],
173
start_point=(0, 0), end_point=(0, 24))
174
stats = {"functions": 0, "classes": 0, "edges": 0}
175
parser._handle_class(class_node, source, "app.py", store, stats)
176
# Should create class node + CONTAINS edge + INHERITS edge
177
assert stats["edges"] == 2
178
179
def test_handle_function(self):
180
parser = self._make_parser()
181
store = _make_store()
182
source = b"def foo(): pass"
183
name_node = _text_node(b"foo")
184
fn_node = MockNode("function_definition", children=[name_node],
185
start_point=(0, 0), end_point=(0, 14))
186
stats = {"functions": 0, "classes": 0, "edges": 0}
187
parser._handle_function(fn_node, source, "app.py", store, stats, class_name=None)
188
assert stats["functions"] == 1
189
assert stats["edges"] == 1
190
store.create_node.assert_called_once()
191
label = store.create_node.call_args[0][0]
192
assert label == NodeLabel.Function
193
194
def test_handle_method(self):
195
parser = self._make_parser()
196
store = _make_store()
197
source = b"def my_method(self): pass"
198
name_node = _text_node(b"my_method")
199
fn_node = MockNode("function_definition", children=[name_node],
200
start_point=(0, 0), end_point=(0, 24))
201
stats = {"functions": 0, "classes": 0, "edges": 0}
202
parser._handle_function(fn_node, source, "app.py", store, stats, class_name="MyClass")
203
label = store.create_node.call_args[0][0]
204
assert label == NodeLabel.Method
205
206
def test_handle_function_no_identifier(self):
207
parser = self._make_parser()
208
store = _make_store()
209
fn_node = MockNode("function_definition", children=[
210
MockNode("keyword", b"def")
211
], start_point=(0, 0), end_point=(0, 0))
212
stats = {"functions": 0, "classes": 0, "edges": 0}
213
parser._handle_function(fn_node, b"def", "app.py", store, stats, class_name=None)
214
assert stats["functions"] == 0
215
216
def test_extract_calls(self):
217
parser = self._make_parser()
218
store = _make_store()
219
source = b"def foo():\n bar()\n"
220
221
callee = _text_node(b"bar")
222
call_node = MockNode("call", children=[callee])
223
block = MockNode("block", children=[call_node])
224
fn_node = MockNode("function_definition", children=[block])
225
226
stats = {"functions": 0, "classes": 0, "edges": 0}
227
parser._extract_calls(fn_node, source, "app.py", "foo", NodeLabel.Function, store, stats)
228
store.create_edge.assert_called_once()
229
assert stats["edges"] == 1
230
231
def test_extract_calls_no_block(self):
232
parser = self._make_parser()
233
store = _make_store()
234
fn_node = MockNode("function_definition", children=[])
235
stats = {"functions": 0, "classes": 0, "edges": 0}
236
parser._extract_calls(fn_node, b"", "app.py", "foo", NodeLabel.Function, store, stats)
237
store.create_edge.assert_not_called()
238
239
def test_walk_dispatches_import(self):
240
parser = self._make_parser()
241
store = _make_store()
242
dotted = _text_node(b"sys", "dotted_name")
243
import_node = MockNode("import_statement", children=[dotted], start_point=(0, 0))
244
root = MockNode("module", children=[import_node])
245
stats = {"functions": 0, "classes": 0, "edges": 0}
246
parser._walk(root, b"import sys", "app.py", store, stats, class_name=None)
247
assert stats["edges"] == 1
248
249
def test_walk_dispatches_class(self):
250
parser = self._make_parser()
251
store = _make_store()
252
name_node = _text_node(b"MyClass")
253
class_node = MockNode("class_definition", children=[name_node],
254
start_point=(0, 0), end_point=(0, 0))
255
root = MockNode("module", children=[class_node])
256
stats = {"functions": 0, "classes": 0, "edges": 0}
257
parser._walk(root, b"class MyClass: pass", "app.py", store, stats, class_name=None)
258
assert stats["classes"] == 1
259
260
def test_walk_dispatches_function(self):
261
parser = self._make_parser()
262
store = _make_store()
263
name_node = _text_node(b"my_fn")
264
fn_node = MockNode("function_definition", children=[name_node],
265
start_point=(0, 0), end_point=(0, 0))
266
root = MockNode("module", children=[fn_node])
267
stats = {"functions": 0, "classes": 0, "edges": 0}
268
parser._walk(root, b"def my_fn(): pass", "app.py", store, stats, class_name=None)
269
assert stats["functions"] == 1
270
271
272
# ── _get_python_language happy path ──────────────────────────────────────────
273
274
class TestGetPythonLanguageHappyPath:
275
def test_returns_language_object(self):
276
from navegador.ingestion.python import _get_python_language
277
mock_tspy = MagicMock()
278
mock_ts = MagicMock()
279
with patch.dict("sys.modules", {
280
"tree_sitter_python": mock_tspy,
281
"tree_sitter": mock_ts,
282
}):
283
result = _get_python_language()
284
assert result is mock_ts.Language.return_value
285
286
287
# ── _get_parser ───────────────────────────────────────────────────────────────
288
289
class TestGetParserHappyPath:
290
def test_returns_parser(self):
291
from navegador.ingestion.python import _get_parser
292
mock_tspy = MagicMock()
293
mock_ts = MagicMock()
294
with patch.dict("sys.modules", {
295
"tree_sitter_python": mock_tspy,
296
"tree_sitter": mock_ts,
297
}):
298
result = _get_parser()
299
assert result is mock_ts.Parser.return_value
300
301
302
# ── parse_file ────────────────────────────────────────────────────────────────
303
304
class TestPythonParseFile:
305
def _make_parser(self):
306
from navegador.ingestion.python import PythonParser
307
with patch("navegador.ingestion.python._get_parser") as mock_get:
308
mock_get.return_value = MagicMock()
309
parser = PythonParser()
310
return parser
311
312
def test_parse_file_creates_file_node(self):
313
import tempfile
314
from pathlib import Path
315
parser = self._make_parser()
316
store = MagicMock()
317
store.query.return_value = MagicMock(result_set=[])
318
mock_tree = MagicMock()
319
mock_tree.root_node.type = "module"
320
mock_tree.root_node.children = []
321
parser._parser.parse.return_value = mock_tree
322
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
323
f.write(b"x = 1\n")
324
fpath = Path(f.name)
325
try:
326
stats = parser.parse_file(fpath, fpath.parent, store)
327
store.create_node.assert_called_once()
328
call = store.create_node.call_args[0]
329
from navegador.graph.schema import NodeLabel
330
assert call[0] == NodeLabel.File
331
assert call[1]["language"] == "python"
332
assert isinstance(stats, dict)
333
finally:
334
fpath.unlink()
335
336
337
# ── _handle_import_from ───────────────────────────────────────────────────────
338
339
class TestHandleImportFrom:
340
def _make_parser(self):
341
from navegador.ingestion.python import PythonParser
342
with patch("navegador.ingestion.python._get_parser") as mock_get:
343
mock_get.return_value = MagicMock()
344
parser = PythonParser()
345
return parser
346
347
def test_handle_import_from_with_member(self):
348
parser = self._make_parser()
349
store = MagicMock()
350
stats = {"functions": 0, "classes": 0, "edges": 0}
351
combined = b"os.pathjoin"
352
module_node3 = MockNode("dotted_name", start_byte=0, end_byte=7)
353
member_node3 = MockNode("import_from_member", start_byte=7, end_byte=11)
354
node3 = MockNode("import_from_statement",
355
children=[module_node3, member_node3],
356
start_point=(0, 0))
357
parser._handle_import_from(node3, combined, "app.py", store, stats)
358
store.create_node.assert_called_once()
359
store.create_edge.assert_called_once()
360
assert stats["edges"] == 1
361
362
def test_handle_import_from_no_member(self):
363
parser = self._make_parser()
364
store = MagicMock()
365
# No import_from_member children — nothing should be created
366
module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
367
node = MockNode("import_from_statement",
368
children=[module_node],
369
start_point=(0, 0))
370
stats = {"functions": 0, "classes": 0, "edges": 0}
371
parser._handle_import_from(node, b"os.path", "app.py", store, stats)
372
store.create_node.assert_not_called()
373
assert stats["edges"] == 0
374
375
def test_walk_dispatches_import_from(self):
376
parser = self._make_parser()
377
store = MagicMock()
378
source = b"os.pathjoin"
379
module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
380
member_node = MockNode("import_from_member", start_byte=7, end_byte=11)
381
import_from = MockNode("import_from_statement",
382
children=[module_node, member_node],
383
start_point=(0, 0))
384
root = MockNode("module", children=[import_from])
385
stats = {"functions": 0, "classes": 0, "edges": 0}
386
parser._walk(root, source, "app.py", store, stats, class_name=None)
387
assert stats["edges"] == 1
388
389
390
# ── _handle_class with body ───────────────────────────────────────────────────
391
392
class TestHandleClassWithBody:
393
def _make_parser(self):
394
from navegador.ingestion.python import PythonParser
395
with patch("navegador.ingestion.python._get_parser") as mock_get:
396
mock_get.return_value = MagicMock()
397
parser = PythonParser()
398
return parser
399
400
def test_handle_class_with_method_in_body(self):
401
parser = self._make_parser()
402
store = MagicMock()
403
source = b"method"
404
name_node = MockNode("identifier", start_byte=0, end_byte=5)
405
# Method inside the class body
406
method_name = MockNode("identifier", start_byte=0, end_byte=6)
407
fn_node = MockNode("function_definition",
408
children=[method_name],
409
start_point=(1, 4), end_point=(1, 20))
410
body = MockNode("block", children=[fn_node])
411
class_node = MockNode("class_definition",
412
children=[name_node, body],
413
start_point=(0, 0), end_point=(2, 0))
414
stats = {"functions": 0, "classes": 0, "edges": 0}
415
parser._handle_class(class_node, source, "app.py", store, stats)
416
# class node + method node both created
417
assert stats["classes"] == 1
418
assert stats["functions"] == 1
419

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button