rbtr-lang-python 2026.7.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbtr_lang_python/__init__.py +1 -0
- rbtr_lang_python/plugin.py +120 -0
- rbtr_lang_python/py.typed +0 -0
- rbtr_lang_python/python.scm +59 -0
- rbtr_lang_python/tests/__init__.py +0 -0
- rbtr_lang_python/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +3 -0
- rbtr_lang_python/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +470 -0
- rbtr_lang_python/tests/cases_docstrings.py +174 -0
- rbtr_lang_python/tests/cases_extraction.py +572 -0
- rbtr_lang_python/tests/samples/python/config.py +3 -0
- rbtr_lang_python/tests/samples/python/python.py +84 -0
- rbtr_lang_python/tests/test_docstrings.py +54 -0
- rbtr_lang_python/tests/test_extraction.py +110 -0
- rbtr_lang_python/tests/test_samples.py +83 -0
- rbtr_lang_python-2026.7.0.dev0.dist-info/METADATA +8 -0
- rbtr_lang_python-2026.7.0.dev0.dist-info/RECORD +18 -0
- rbtr_lang_python-2026.7.0.dev0.dist-info/WHEEL +4 -0
- rbtr_lang_python-2026.7.0.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Python extraction test cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pytest_cases import case
|
|
7
|
+
|
|
8
|
+
type SymbolCase = tuple[str, str, list[tuple[str, str, str]]]
|
|
9
|
+
type ImportCase = tuple[str, str, dict[str, str]]
|
|
10
|
+
type MultiImportCase = tuple[str, str, int, list[dict[str, str]]]
|
|
11
|
+
type MixedCase = tuple[str, str, set[str], list[tuple[str, str]]]
|
|
12
|
+
|
|
13
|
+
_xfail_nested = pytest.mark.xfail(
|
|
14
|
+
reason="nested/chained destructuring unsupported — no query-only recursion",
|
|
15
|
+
strict=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@case(tags=["symbol"])
|
|
20
|
+
def case_py_simple_function() -> SymbolCase:
|
|
21
|
+
"""Top-level function."""
|
|
22
|
+
src = """\
|
|
23
|
+
def hello():
|
|
24
|
+
pass
|
|
25
|
+
"""
|
|
26
|
+
return "python", src, [("function", "hello", "")]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@case(tags=["symbol"])
|
|
30
|
+
def case_py_function_with_args() -> SymbolCase:
|
|
31
|
+
"""Function with parameters."""
|
|
32
|
+
src = """\
|
|
33
|
+
def add(a, b):
|
|
34
|
+
return a + b
|
|
35
|
+
"""
|
|
36
|
+
return "python", src, [("function", "add", "")]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@case(tags=["symbol"])
|
|
40
|
+
def case_py_async_function() -> SymbolCase:
|
|
41
|
+
"""Async function."""
|
|
42
|
+
src = """\
|
|
43
|
+
async def fetch():
|
|
44
|
+
pass
|
|
45
|
+
"""
|
|
46
|
+
return "python", src, [("function", "fetch", "")]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@case(tags=["symbol"])
|
|
50
|
+
def case_py_multiple_functions() -> SymbolCase:
|
|
51
|
+
"""Multiple functions — all present."""
|
|
52
|
+
src = """\
|
|
53
|
+
def foo():
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def bar():
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def baz():
|
|
60
|
+
pass
|
|
61
|
+
"""
|
|
62
|
+
return (
|
|
63
|
+
"python",
|
|
64
|
+
src,
|
|
65
|
+
[
|
|
66
|
+
("function", "foo", ""),
|
|
67
|
+
("function", "bar", ""),
|
|
68
|
+
("function", "baz", ""),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@case(tags=["symbol"])
|
|
74
|
+
def case_py_decorated_function() -> SymbolCase:
|
|
75
|
+
"""Decorated function."""
|
|
76
|
+
src = """\
|
|
77
|
+
@decorator
|
|
78
|
+
def wrapped():
|
|
79
|
+
pass
|
|
80
|
+
"""
|
|
81
|
+
return "python", src, [("function", "wrapped", "")]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@case(tags=["symbol"])
|
|
85
|
+
def case_py_simple_class() -> SymbolCase:
|
|
86
|
+
"""Top-level class."""
|
|
87
|
+
src = """\
|
|
88
|
+
class Foo:
|
|
89
|
+
pass
|
|
90
|
+
"""
|
|
91
|
+
return "python", src, [("class", "Foo", "")]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@case(tags=["symbol"])
|
|
95
|
+
def case_py_class_with_bases() -> SymbolCase:
|
|
96
|
+
"""Class with inheritance."""
|
|
97
|
+
src = """\
|
|
98
|
+
class Bar(Foo, Mixin):
|
|
99
|
+
pass
|
|
100
|
+
"""
|
|
101
|
+
return "python", src, [("class", "Bar", "")]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@case(tags=["symbol"])
|
|
105
|
+
def case_py_decorated_class() -> SymbolCase:
|
|
106
|
+
"""Decorated class."""
|
|
107
|
+
src = """\
|
|
108
|
+
@dataclass
|
|
109
|
+
class Config:
|
|
110
|
+
name: str
|
|
111
|
+
"""
|
|
112
|
+
return "python", src, [("class", "Config", "")]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@case(tags=["symbol"])
|
|
116
|
+
def case_py_multiple_classes() -> SymbolCase:
|
|
117
|
+
"""Multiple classes."""
|
|
118
|
+
src = """\
|
|
119
|
+
class A:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
class B:
|
|
123
|
+
pass
|
|
124
|
+
"""
|
|
125
|
+
return "python", src, [("class", "A", ""), ("class", "B", "")]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@case(tags=["symbol"])
|
|
129
|
+
def case_py_method_in_class() -> SymbolCase:
|
|
130
|
+
"""Method scoped to class."""
|
|
131
|
+
src = """\
|
|
132
|
+
class Foo:
|
|
133
|
+
def bar(self):
|
|
134
|
+
pass
|
|
135
|
+
"""
|
|
136
|
+
return "python", src, [("method", "bar", "Foo")]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@case(tags=["symbol"])
|
|
140
|
+
def case_py_multiple_methods() -> SymbolCase:
|
|
141
|
+
"""Multiple methods in one class."""
|
|
142
|
+
src = """\
|
|
143
|
+
class Svc:
|
|
144
|
+
def start(self):
|
|
145
|
+
pass
|
|
146
|
+
def stop(self):
|
|
147
|
+
pass
|
|
148
|
+
"""
|
|
149
|
+
return "python", src, [("method", "start", "Svc"), ("method", "stop", "Svc")]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@case(tags=["symbol"])
|
|
153
|
+
def case_py_nested_class_method() -> SymbolCase:
|
|
154
|
+
"""Method in a nested class carries the full class path."""
|
|
155
|
+
src = """\
|
|
156
|
+
class Outer:
|
|
157
|
+
class Inner:
|
|
158
|
+
def deep(self):
|
|
159
|
+
pass
|
|
160
|
+
"""
|
|
161
|
+
return "python", src, [("method", "deep", "Outer::Inner")]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@case(tags=["symbol"])
|
|
165
|
+
def case_py_top_level_not_method() -> SymbolCase:
|
|
166
|
+
"""Function after a class is not scoped."""
|
|
167
|
+
src = """\
|
|
168
|
+
class Foo:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def standalone():
|
|
172
|
+
pass
|
|
173
|
+
"""
|
|
174
|
+
return "python", src, [("function", "standalone", "")]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@case(tags=["symbol"])
|
|
178
|
+
def case_py_static_method_scoped() -> SymbolCase:
|
|
179
|
+
"""Staticmethod still scoped to class."""
|
|
180
|
+
src = """\
|
|
181
|
+
class Svc:
|
|
182
|
+
@staticmethod
|
|
183
|
+
def create():
|
|
184
|
+
pass
|
|
185
|
+
"""
|
|
186
|
+
return "python", src, [("method", "create", "Svc")]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@case(tags=["symbol"])
|
|
190
|
+
def case_py_closure_in_function() -> SymbolCase:
|
|
191
|
+
"""A closure is addressed by its enclosing function and stays a function.
|
|
192
|
+
|
|
193
|
+
Function nesting must contribute to the address (inner `handler`
|
|
194
|
+
→ scope "make_adder"), and a function nested in a function must
|
|
195
|
+
remain kind=function — it is not a method.
|
|
196
|
+
"""
|
|
197
|
+
src = """\
|
|
198
|
+
def make_adder(n):
|
|
199
|
+
def handler():
|
|
200
|
+
return n + 1
|
|
201
|
+
return handler
|
|
202
|
+
"""
|
|
203
|
+
return "python", src, [("function", "handler", "make_adder")]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@case(tags=["symbol"])
|
|
207
|
+
def case_py_closure_in_method_stays_function() -> SymbolCase:
|
|
208
|
+
"""A function nested in a method is addressed fully and NOT promoted.
|
|
209
|
+
|
|
210
|
+
The enclosing scope path is "Svc::start" and `cb` must stay
|
|
211
|
+
kind=function — promotion follows the nearest *enclosing scope
|
|
212
|
+
node's* type (a class), not merely a non-empty scope.
|
|
213
|
+
"""
|
|
214
|
+
src = """\
|
|
215
|
+
class Svc:
|
|
216
|
+
def start(self):
|
|
217
|
+
def cb():
|
|
218
|
+
return 1
|
|
219
|
+
return cb
|
|
220
|
+
"""
|
|
221
|
+
return "python", src, [("function", "cb", "Svc::start")]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@case(tags=["symbol"])
|
|
225
|
+
def case_py_repeated_nested_name() -> SymbolCase:
|
|
226
|
+
"""Repeated scope names compose without collapsing.
|
|
227
|
+
|
|
228
|
+
Three classes all named `Node` nest; the innermost method's path
|
|
229
|
+
keeps every level (`Node::Node::Node`), not a deduplicated one.
|
|
230
|
+
"""
|
|
231
|
+
src = """\
|
|
232
|
+
class Node:
|
|
233
|
+
class Node:
|
|
234
|
+
class Node:
|
|
235
|
+
def visit(self):
|
|
236
|
+
pass
|
|
237
|
+
"""
|
|
238
|
+
return "python", src, [("method", "visit", "Node::Node::Node")]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@case(tags=["symbol"])
|
|
242
|
+
def case_py_shadowed_closure() -> SymbolCase:
|
|
243
|
+
"""A closure shadowing its enclosing function's name stays distinct.
|
|
244
|
+
|
|
245
|
+
The inner `process` is addressed `process` (its outer namesake);
|
|
246
|
+
the two share a name but differ in scope, so addressing keeps
|
|
247
|
+
them apart. The inner one is a function, not a method.
|
|
248
|
+
"""
|
|
249
|
+
src = """\
|
|
250
|
+
def process():
|
|
251
|
+
def process():
|
|
252
|
+
return 1
|
|
253
|
+
return process
|
|
254
|
+
"""
|
|
255
|
+
return "python", src, [("function", "process", "process"), ("function", "process", "")]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@case(tags=["symbol"])
|
|
259
|
+
def case_py_mixed_deep_nesting() -> SymbolCase:
|
|
260
|
+
"""Mixed class/method/closure/class chain composes the full path.
|
|
261
|
+
|
|
262
|
+
`deep` sits in a local class, in a closure, in a method, in a
|
|
263
|
+
class — so its path mixes every scope kind. Promotion follows the
|
|
264
|
+
*nearest* scope: `helper` (in a method) is a function, `deep` (in
|
|
265
|
+
a class) is a method.
|
|
266
|
+
"""
|
|
267
|
+
src = """\
|
|
268
|
+
class Outer:
|
|
269
|
+
def run(self):
|
|
270
|
+
def helper():
|
|
271
|
+
class Local:
|
|
272
|
+
def deep(self):
|
|
273
|
+
pass
|
|
274
|
+
return Local
|
|
275
|
+
return helper
|
|
276
|
+
"""
|
|
277
|
+
return (
|
|
278
|
+
"python",
|
|
279
|
+
src,
|
|
280
|
+
[
|
|
281
|
+
("method", "run", "Outer"),
|
|
282
|
+
("function", "helper", "Outer::run"),
|
|
283
|
+
("class", "Local", "Outer::run::helper"),
|
|
284
|
+
("method", "deep", "Outer::run::helper::Local"),
|
|
285
|
+
],
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@case(tags=["symbol"])
|
|
290
|
+
def case_py_method_named_like_class() -> SymbolCase:
|
|
291
|
+
"""A method whose name equals its class is addressed `Node::`, not merged.
|
|
292
|
+
|
|
293
|
+
The method `Node` inside class `Node` is `(method, Node, Node)` —
|
|
294
|
+
name and scope segment coincide but are different objects; the
|
|
295
|
+
address keeps both.
|
|
296
|
+
"""
|
|
297
|
+
src = """\
|
|
298
|
+
class Node:
|
|
299
|
+
def Node(self):
|
|
300
|
+
return 1
|
|
301
|
+
"""
|
|
302
|
+
return "python", src, [("class", "Node", ""), ("method", "Node", "Node")]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@case(tags=["symbol"])
|
|
306
|
+
def case_py_method_named_like_nested_class() -> SymbolCase:
|
|
307
|
+
"""Name equal to a *repeated* scope segment still composes fully.
|
|
308
|
+
|
|
309
|
+
A class `Node` in a class `Node` with a method `Node`: the method
|
|
310
|
+
is `(method, Node, Node::Node)` and the inner class is
|
|
311
|
+
`(class, Node, Node)`.
|
|
312
|
+
"""
|
|
313
|
+
src = """\
|
|
314
|
+
class Node:
|
|
315
|
+
class Node:
|
|
316
|
+
def Node(self):
|
|
317
|
+
return 1
|
|
318
|
+
"""
|
|
319
|
+
return "python", src, [("class", "Node", "Node"), ("method", "Node", "Node::Node")]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@case(tags=["symbol"])
|
|
323
|
+
def case_py_function_and_class_same_name() -> SymbolCase:
|
|
324
|
+
"""A function and a class sharing a name at module scope both extract.
|
|
325
|
+
|
|
326
|
+
Different objects (`function` and `class`) collide on identity
|
|
327
|
+
`(name, scope)` = `(Cache, "")` — the residual same-scope
|
|
328
|
+
collision addressing cannot split (kind is not part of identity).
|
|
329
|
+
Both must appear; the diff's content-set backstop keeps them apart.
|
|
330
|
+
"""
|
|
331
|
+
src = """\
|
|
332
|
+
def Cache():
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
class Cache:
|
|
336
|
+
pass
|
|
337
|
+
"""
|
|
338
|
+
return "python", src, [("function", "Cache", ""), ("class", "Cache", "")]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@case(tags=["symbol"])
|
|
342
|
+
def case_py_module_constant() -> SymbolCase:
|
|
343
|
+
"""Module-level constant."""
|
|
344
|
+
src = """\
|
|
345
|
+
MAX_SIZE = 100
|
|
346
|
+
"""
|
|
347
|
+
return "python", src, [("variable", "MAX_SIZE", "")]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@case(tags=["symbol"])
|
|
351
|
+
def case_py_module_singleton() -> SymbolCase:
|
|
352
|
+
"""Module-level lowercase singleton (the motivating case)."""
|
|
353
|
+
src = """\
|
|
354
|
+
config = Config()
|
|
355
|
+
"""
|
|
356
|
+
return "python", src, [("variable", "config", "")]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@case(tags=["symbol"])
|
|
360
|
+
def case_py_annotated_module_var() -> SymbolCase:
|
|
361
|
+
"""Module-level annotated assignment."""
|
|
362
|
+
src = """\
|
|
363
|
+
TIMEOUT: int = 30
|
|
364
|
+
"""
|
|
365
|
+
return "python", src, [("variable", "TIMEOUT", "")]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@case(tags=["import"])
|
|
369
|
+
def case_py_import_bare() -> ImportCase:
|
|
370
|
+
"""import os."""
|
|
371
|
+
return "python", "import os\n", {"module": "os"}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@case(tags=["import"])
|
|
375
|
+
def case_py_import_dotted() -> ImportCase:
|
|
376
|
+
"""import os.path."""
|
|
377
|
+
return "python", "import os.path\n", {"module": "os.path"}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@case(tags=["import"])
|
|
381
|
+
def case_py_import_deeply_nested() -> ImportCase:
|
|
382
|
+
"""import a.b.c.d."""
|
|
383
|
+
return "python", "import a.b.c.d\n", {"module": "a.b.c.d"}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@case(tags=["import"])
|
|
387
|
+
def case_py_from_import_single() -> ImportCase:
|
|
388
|
+
"""from pathlib import Path."""
|
|
389
|
+
return "python", "from pathlib import Path\n", {"module": "pathlib", "names": "Path"}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@case(tags=["import"])
|
|
393
|
+
def case_py_from_import_multiple() -> ImportCase:
|
|
394
|
+
"""from module import multiple names."""
|
|
395
|
+
return (
|
|
396
|
+
"python",
|
|
397
|
+
"from rbtr.index.models import Chunk, Edge\n",
|
|
398
|
+
{"module": "rbtr.index.models", "names": "Chunk,Edge"},
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@case(tags=["import"])
|
|
403
|
+
def case_py_from_import_aliased() -> ImportCase:
|
|
404
|
+
"""Aliased import extracts original name."""
|
|
405
|
+
return (
|
|
406
|
+
"python",
|
|
407
|
+
"from .models import Chunk as C\n",
|
|
408
|
+
{"dots": "1", "module": "models", "names": "Chunk"},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@case(tags=["import"])
|
|
413
|
+
def case_py_from_import_multiple_aliased() -> ImportCase:
|
|
414
|
+
"""Multiple aliased imports."""
|
|
415
|
+
return (
|
|
416
|
+
"python",
|
|
417
|
+
"from models import Foo as F, Bar as B\n",
|
|
418
|
+
{"module": "models", "names": "Foo,Bar"},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@case(tags=["import"])
|
|
423
|
+
def case_py_relative_dot_with_module() -> ImportCase:
|
|
424
|
+
"""from .models import Chunk."""
|
|
425
|
+
return (
|
|
426
|
+
"python",
|
|
427
|
+
"from .models import Chunk\n",
|
|
428
|
+
{"dots": "1", "module": "models", "names": "Chunk"},
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@case(tags=["import"])
|
|
433
|
+
def case_py_relative_dotdot() -> ImportCase:
|
|
434
|
+
"""from ..core import engine."""
|
|
435
|
+
return (
|
|
436
|
+
"python",
|
|
437
|
+
"from ..core import engine\n",
|
|
438
|
+
{"dots": "2", "module": "core", "names": "engine"},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@case(tags=["import"])
|
|
443
|
+
def case_py_relative_dot_only() -> ImportCase:
|
|
444
|
+
"""from . import utils — no module key."""
|
|
445
|
+
return "python", "from . import utils\n", {"dots": "1", "names": "utils"}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@case(tags=["import"])
|
|
449
|
+
def case_py_relative_three_dots() -> ImportCase:
|
|
450
|
+
"""from ...lib import helper."""
|
|
451
|
+
return (
|
|
452
|
+
"python",
|
|
453
|
+
"from ...lib import helper\n",
|
|
454
|
+
{"dots": "3", "module": "lib", "names": "helper"},
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@case(tags=["import"])
|
|
459
|
+
def case_py_import_star() -> ImportCase:
|
|
460
|
+
"""from os.path import * — no names key."""
|
|
461
|
+
return "python", "from os.path import *\n", {"module": "os.path"}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@case(tags=["import"])
|
|
465
|
+
def case_py_import_inside_function() -> ImportCase:
|
|
466
|
+
"""Nested import still captured."""
|
|
467
|
+
src = """\
|
|
468
|
+
def f():
|
|
469
|
+
import json
|
|
470
|
+
"""
|
|
471
|
+
return "python", src, {"module": "json"}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@case(tags=["import"])
|
|
475
|
+
def case_py_import_inside_class() -> ImportCase:
|
|
476
|
+
"""Import inside class body."""
|
|
477
|
+
src = """\
|
|
478
|
+
class C:
|
|
479
|
+
from collections import OrderedDict
|
|
480
|
+
"""
|
|
481
|
+
return "python", src, {"module": "collections", "names": "OrderedDict"}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@case(tags=["multi_import"])
|
|
485
|
+
def case_py_multiple_imports() -> MultiImportCase:
|
|
486
|
+
"""Two bare import statements."""
|
|
487
|
+
src = """\
|
|
488
|
+
import os
|
|
489
|
+
import sys
|
|
490
|
+
"""
|
|
491
|
+
return "python", src, 2, [{"module": "os"}, {"module": "sys"}]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@case(tags=["mixed"])
|
|
495
|
+
def case_py_full_module() -> MixedCase:
|
|
496
|
+
"""Realistic module with all symbol types and docstrings.
|
|
497
|
+
|
|
498
|
+
Docstrings are PEP-257 style on every symbol. The
|
|
499
|
+
expected-tuple pins symbol extraction invariants; content
|
|
500
|
+
invariants are covered separately by `test_docstrings.py`.
|
|
501
|
+
Adding docs here exercises the realistic shape that
|
|
502
|
+
production Python code has.
|
|
503
|
+
"""
|
|
504
|
+
src = '''\
|
|
505
|
+
"""Module-level docstring for the Config helper."""
|
|
506
|
+
|
|
507
|
+
import os
|
|
508
|
+
from pathlib import Path
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class Config:
|
|
512
|
+
"""Runtime configuration."""
|
|
513
|
+
|
|
514
|
+
def __init__(self):
|
|
515
|
+
"""Initialise with defaults."""
|
|
516
|
+
pass
|
|
517
|
+
|
|
518
|
+
def load(self):
|
|
519
|
+
"""Load from disk."""
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def main():
|
|
524
|
+
"""Entry point."""
|
|
525
|
+
pass
|
|
526
|
+
'''
|
|
527
|
+
return (
|
|
528
|
+
"python",
|
|
529
|
+
src,
|
|
530
|
+
{"import", "class", "method", "function"},
|
|
531
|
+
[("__init__", "Config"), ("load", "Config")],
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@case(tags=["symbol"])
|
|
536
|
+
def case_py_tuple_unpack() -> SymbolCase:
|
|
537
|
+
"""Flat tuple unpacking."""
|
|
538
|
+
return "python", "a, b = compute()\n", [("variable", "a", ""), ("variable", "b", "")]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@case(tags=["symbol"])
|
|
542
|
+
def case_py_paren_tuple_unpack() -> SymbolCase:
|
|
543
|
+
"""Parenthesised tuple target."""
|
|
544
|
+
return "python", "(a, b) = compute()\n", [("variable", "a", ""), ("variable", "b", "")]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@case(tags=["symbol"])
|
|
548
|
+
def case_py_list_unpack() -> SymbolCase:
|
|
549
|
+
"""List-pattern target."""
|
|
550
|
+
return "python", "[a, b] = compute()\n", [("variable", "a", ""), ("variable", "b", "")]
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@case(tags=["symbol"])
|
|
554
|
+
def case_py_star_unpack() -> SymbolCase:
|
|
555
|
+
"""Starred target."""
|
|
556
|
+
return "python", "a, *rest = compute()\n", [("variable", "a", ""), ("variable", "rest", "")]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@case(tags=["symbol"], marks=_xfail_nested)
|
|
560
|
+
def case_py_nested_unpack_xfail() -> SymbolCase:
|
|
561
|
+
"""Nested tuple unpacking — only the outer level is captured today."""
|
|
562
|
+
return (
|
|
563
|
+
"python",
|
|
564
|
+
"(a, b), c = f()\n",
|
|
565
|
+
[("variable", "a", ""), ("variable", "b", ""), ("variable", "c", "")],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@case(tags=["symbol"], marks=_xfail_nested)
|
|
570
|
+
def case_py_chained_assignment_xfail() -> SymbolCase:
|
|
571
|
+
"""Chained assignment — only the first target is captured today."""
|
|
572
|
+
return "python", "a = b = f()\n", [("variable", "a", ""), ("variable", "b", "")]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Top-of-file banner comment, attached to nothing.
|
|
2
|
+
# Second banner line, same block.
|
|
3
|
+
|
|
4
|
+
"""Greeter — formats greetings for named recipients.
|
|
5
|
+
|
|
6
|
+
A sample module exercising the constructs the python plugin extracts:
|
|
7
|
+
functions (sync, async, decorated), classes, methods (instance, property,
|
|
8
|
+
static, class), module-level variables (including tuple unpacking and
|
|
9
|
+
annotated assignments), nested functions (scoped to their parent), PEP 695
|
|
10
|
+
`type` aliases (as classes), the import styles that carry distinct
|
|
11
|
+
metadata, and standalone / leading comments.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path as P
|
|
19
|
+
|
|
20
|
+
from .config import LOCALE
|
|
21
|
+
|
|
22
|
+
type GreetingList = list[str]
|
|
23
|
+
|
|
24
|
+
DEFAULT_GREETING = "Hello"
|
|
25
|
+
LOCALES, FALLBACK = ("en", "fr"), "en"
|
|
26
|
+
MAX_RECIPIENTS: int = 100 # trailing comment: not folded, its own chunk
|
|
27
|
+
|
|
28
|
+
# Section: greeting helpers.
|
|
29
|
+
# A standalone block between definitions.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Leading doc comment folded into format_greeting.
|
|
33
|
+
def format_greeting(name: str) -> str:
|
|
34
|
+
"""Return a greeting for ``name`` in the configured locale."""
|
|
35
|
+
|
|
36
|
+
def normalise(raw: str) -> str:
|
|
37
|
+
"""Trim and title-case a raw recipient name."""
|
|
38
|
+
return raw.strip().title()
|
|
39
|
+
|
|
40
|
+
return f"{DEFAULT_GREETING}, {normalise(name)} ({LOCALE})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def fetch_remote_greeting(url: str) -> str:
|
|
44
|
+
"""Fetch a greeting template from a remote source."""
|
|
45
|
+
return os.environ.get("GREETING", DEFAULT_GREETING)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache
|
|
49
|
+
def cached_default() -> str:
|
|
50
|
+
"""Cache and return the default greeting prefix."""
|
|
51
|
+
return DEFAULT_GREETING
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Greeter:
|
|
55
|
+
"""Stateful greeter holding a prefix and recipient log."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, prefix: str = DEFAULT_GREETING) -> None:
|
|
58
|
+
self.prefix = prefix
|
|
59
|
+
self._seen: list[str] = []
|
|
60
|
+
|
|
61
|
+
def greet(self, name: str) -> str:
|
|
62
|
+
"""Greet ``name`` and record the recipient."""
|
|
63
|
+
self._seen.append(name)
|
|
64
|
+
return format_greeting(name)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def seen(self) -> list[str]:
|
|
68
|
+
"""Recipients greeted so far."""
|
|
69
|
+
return list(self._seen)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def shout(message: str) -> str:
|
|
73
|
+
"""Upper-case a message."""
|
|
74
|
+
return message.upper()
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def default(cls) -> Greeter:
|
|
78
|
+
"""Build a greeter with the default prefix."""
|
|
79
|
+
return cls(DEFAULT_GREETING)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def config_path() -> P:
|
|
83
|
+
"""Return the path to the greeter config file."""
|
|
84
|
+
return P.home() / ".greeter"
|