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.
@@ -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,3 @@
1
+ """Configuration values for the greeter."""
2
+
3
+ LOCALE = "en"
@@ -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"