radixcodemap 0.2.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jdotpy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: radixcodemap
3
+ Version: 0.2.0
4
+ Summary: Summarize code repositories quickly and (soon) with multiple verbosity levels
5
+ Home-page: https://github.com/jdotpy/radix-map
6
+ Download-URL: https://github.com/jdotpy/radix-map/tarball/master
7
+ Author: KJ
8
+ Author-email: jdotpy@users.noreply.github.com
9
+ Keywords: tools
10
+ Classifier: Programming Language :: Python :: 3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: tree-sitter
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Requires-Dist: twine; extra == "dev"
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: download-url
23
+ Dynamic: home-page
24
+ Dynamic: keywords
25
+ Dynamic: license-file
26
+ Dynamic: provides-extra
27
+ Dynamic: requires-dist
28
+ Dynamic: summary
29
+
30
+
31
+
32
+ # Radix: Configurable SourceCode summarizer
33
+
34
+ Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
35
+
36
+
37
+ ## Installation
38
+ ---
39
+
40
+ 1. **Clone the repository:**
41
+ ```bash
42
+ git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
43
+ cd radix-map
44
+ pip install .
45
+ ```
46
+
47
+
48
+ 2. **Install Dependencies based on code you plan on using:**
49
+ ```bash
50
+ pip install tree-sitter-python
51
+ pip install tree-sitter-go
52
+ pip install tree-sitter-javascript
53
+ ```
54
+
55
+
56
+ ## Use
57
+ ---
58
+
59
+ ```bash
60
+ radix map .
61
+ ```
62
+
63
+ Example output:
64
+ ```bash
65
+ #tests/test_integration.py
66
+ └── ƒ get_test_pairs()
67
+
68
+ #tests/snapshots/python_ex1.py
69
+ ├── ƒ global_helper()
70
+ └── ○ class DataProcessor
71
+ ├── ƒ __init__(self, source: str)
72
+ ├── ƒ process(self)
73
+ └── ƒ _validate(self)
74
+
75
+ #radix/scanner.py
76
+ └── ○ class ProjectScanner
77
+ ├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
78
+ ├── ƒ is_visible(self, path: Path)
79
+ └── ƒ scan(self, target: str)
80
+ ```
81
+
82
+ ## Supported Languages
83
+
84
+
85
+ | Language | Status | Package Requirement |
86
+ |---|---|---|
87
+ | Python | ✅ functions & classes | tree-sitter-python |
88
+ | Go | 🚧 | tree-sitter-go |
89
+ | JavaScript | 🚧 | tree-sitter-javascript |
90
+
91
+ ```
92
+
@@ -0,0 +1,63 @@
1
+
2
+
3
+ # Radix: Configurable SourceCode summarizer
4
+
5
+ Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
6
+
7
+
8
+ ## Installation
9
+ ---
10
+
11
+ 1. **Clone the repository:**
12
+ ```bash
13
+ git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
14
+ cd radix-map
15
+ pip install .
16
+ ```
17
+
18
+
19
+ 2. **Install Dependencies based on code you plan on using:**
20
+ ```bash
21
+ pip install tree-sitter-python
22
+ pip install tree-sitter-go
23
+ pip install tree-sitter-javascript
24
+ ```
25
+
26
+
27
+ ## Use
28
+ ---
29
+
30
+ ```bash
31
+ radix map .
32
+ ```
33
+
34
+ Example output:
35
+ ```bash
36
+ #tests/test_integration.py
37
+ └── ƒ get_test_pairs()
38
+
39
+ #tests/snapshots/python_ex1.py
40
+ ├── ƒ global_helper()
41
+ └── ○ class DataProcessor
42
+ ├── ƒ __init__(self, source: str)
43
+ ├── ƒ process(self)
44
+ └── ƒ _validate(self)
45
+
46
+ #radix/scanner.py
47
+ └── ○ class ProjectScanner
48
+ ├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
49
+ ├── ƒ is_visible(self, path: Path)
50
+ └── ƒ scan(self, target: str)
51
+ ```
52
+
53
+ ## Supported Languages
54
+
55
+
56
+ | Language | Status | Package Requirement |
57
+ |---|---|---|
58
+ | Python | ✅ functions & classes | tree-sitter-python |
59
+ | Go | 🚧 | tree-sitter-go |
60
+ | JavaScript | 🚧 | tree-sitter-javascript |
61
+
62
+ ```
63
+
File without changes
@@ -0,0 +1,46 @@
1
+ from . import core
2
+ from . import cli
3
+ import argparse
4
+ import sys
5
+
6
+ def create_parser():
7
+ parser = argparse.ArgumentParser(prog="radix", description="Codebase Mapping Tool")
8
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
9
+
10
+
11
+ # 'registry' subcommand
12
+ registry_parser = subparsers.add_parser("registry", help="Show registered file handlers and their libraries")
13
+
14
+ # 'map' subcommand
15
+ map_parser = subparsers.add_parser("map", help="Generate a structural map of the codebase")
16
+
17
+ # Positional argument
18
+ map_parser.add_argument("path", help="Path to file or directory to scan")
19
+
20
+ # Fine-grained Overrides (Your original flags)
21
+ group = map_parser.add_argument_group("Detail Overrides")
22
+ group.add_argument("--calls", action="store_true", help="Include internal function calls")
23
+ group.add_argument("--lines", action="store_true", help="Include type definitions/properties")
24
+
25
+ # Configuration
26
+ map_parser.add_argument("--max-size", type=int, default=200000,
27
+ help="Skip files larger than this size in bytes")
28
+
29
+
30
+
31
+ return parser
32
+
33
+
34
+ def entrypoint():
35
+ parser = create_parser()
36
+ args = parser.parse_args()
37
+
38
+ if args.command == "map":
39
+ cli.cli_map(args)
40
+ elif args.command == "registry":
41
+ cli.cli_registry(args)
42
+ else:
43
+ parser.print_help()
44
+
45
+ if __name__ == "__main__":
46
+ entrypoint()
@@ -0,0 +1,32 @@
1
+ from . import core
2
+ from . import report
3
+ from .handlers.registry import HandlerRegistry
4
+
5
+ import sys
6
+ import importlib.util
7
+
8
+
9
+ def cli_map(args):
10
+ scanner = core.default_scanner(args.path)
11
+ source = core.default_source(args.path)
12
+ reports_by_file = core.analyze_project(scanner, source, calls=args.calls, lines=args.lines)
13
+ report.display_txt(
14
+ reports_by_file,
15
+ sys.stdout,
16
+ lines=args.lines,
17
+ )
18
+
19
+ def cli_registry(args):
20
+ print('Supported languages:')
21
+ for file_type, info in HandlerRegistry.LIBRARIES.items():
22
+ spec = importlib.util.find_spec(info['lib'])
23
+ has_package = spec is not None
24
+ extra_info = []
25
+ if not has_package:
26
+ status = "Missing"
27
+ extra_info.append('\t')
28
+ extra_info.append(f'to install run `pip install {info["package_name"]}`')
29
+ else:
30
+ status = "Installed"
31
+ print('\t', file_type, '\t', status, *extra_info)
32
+
@@ -0,0 +1,31 @@
1
+ from . import scanner
2
+ from .handlers.registry import HandlerRegistry
3
+
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ def default_scanner(path: str):
8
+ registry = HandlerRegistry()
9
+ s = scanner.ProjectScanner(registry)
10
+ return s
11
+
12
+ def default_source(path):
13
+ return scanner.DiskSource(path)
14
+
15
+ def analyze_project(scanner, source, calls=False, lines=False):
16
+ reports = {}
17
+ for file_path, relative_path, handler, read_func in scanner.scan(source):
18
+ try:
19
+ source_file = handler(file_path, read_func())
20
+ except Exception as e:
21
+ sys.stderr.write(f'Failed to parse file={file_path} with handler={handler.__name__}. Skipping. Error="{str(e)}"')
22
+ continue
23
+
24
+ file_report = {
25
+ "path": str(file_path),
26
+ "lines": source_file.get_line_count(),
27
+ "functions": list(source_file.iter_functions(include_calls=calls)),
28
+ "definitions": list(source_file.iter_definitions(include_methods=True, include_calls=calls)),
29
+ }
30
+ reports[relative_path] = file_report
31
+ return reports
File without changes
@@ -0,0 +1,80 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, field
3
+ from typing import List, Dict, Optional
4
+
5
+ @dataclass
6
+ class Variable:
7
+ name: str
8
+ type_hint: Optional[str] = None
9
+ value_snippet: Optional[str] = None # For constants like MAX_RETRIES = 5
10
+
11
+ def __str__(self):
12
+ type_prefix = ''
13
+ if self.type_hint:
14
+ type_prefix = f'{self.type_hint} '
15
+ value_suffix = ''
16
+ if self.value_snippet:
17
+ value_suffix = f' {self.value_snippet}'
18
+ return f'{type_prefix}{self.name}{value_suffix}'
19
+
20
+ @dataclass
21
+ class Function:
22
+ name: str
23
+ source_lines: tuple[int, int]
24
+ arguments: str = ""
25
+ return_type: str = ""
26
+ is_public: bool = False
27
+ calls: List[str] = field(default_factory=list)
28
+
29
+ def __str__(self):
30
+ return_suffix = ''
31
+ if self.return_type:
32
+ return_suffix = ' ➜ {return_type}'
33
+ return f'ƒ {self.name}({self.arguments}){return_suffix}'
34
+
35
+
36
+ @dataclass
37
+ class Definition:
38
+ name: str
39
+ kind: str # "class", "struct", "interface"
40
+ source_lines: tuple[int, int]
41
+ properties: List[Variable] = field(default_factory=list)
42
+ methods: List[Function] = field(default_factory=list)
43
+
44
+
45
+ def __str__(self):
46
+ return f'{self.kind} {self.name}'
47
+
48
+ class SourceFile(ABC):
49
+ def __init__(self, path: str, code: bytes):
50
+ self.path = path
51
+ self.code = code
52
+ self._tree = self._parse()
53
+
54
+ @abstractmethod
55
+ def get_line_count(self):
56
+ """Fetch total lines in source file"""
57
+ pass
58
+
59
+ @abstractmethod
60
+ def _parse(self):
61
+ """Initialize the Tree-sitter parser for the specific language."""
62
+ pass
63
+
64
+ @abstractmethod
65
+ def iter_definitions(self) -> List[Definition]:
66
+ """
67
+ Returns Classes or Structs.
68
+ Implementation should populate .properties and .methods.
69
+ """
70
+ pass
71
+
72
+ @abstractmethod
73
+ def iter_functions(self) -> List[Function]:
74
+ """Returns top-level functions (logic not bound to a class/struct)."""
75
+ pass
76
+
77
+ @abstractmethod
78
+ def iter_globals(self) -> List[Variable]:
79
+ """Returns top-level constants and global variables."""
80
+ pass
@@ -0,0 +1,126 @@
1
+ from .base import Definition, Function, Variable, SourceFile
2
+ from tree_sitter import Language, Parser
3
+ import tree_sitter_go as tsgo
4
+
5
+ from .tree_utils import ts_get_captures, ts_line_info, one, q
6
+
7
+ class GoSourceFile(SourceFile):
8
+ lang = Language(tsgo.language())
9
+
10
+ def _parse(self):
11
+ self.parser = Parser(self.lang)
12
+ return self.parser.parse(self.code)
13
+
14
+ def _get_text(self, node):
15
+ if node is None:
16
+ return ''
17
+ return self.code[node.start_byte:node.end_byte].decode('utf-8')
18
+
19
+ def get_line_count(self):
20
+ """Fetch total lines in source file"""
21
+ return ts_line_info(self._tree.root_node)['source_lines'][1]
22
+
23
+ def iter_definitions(self, include_calls=False, include_methods=False) -> list[Definition]:
24
+ """Returns Structs and Interfaces, populating their methods."""
25
+ # Query for type declarations (structs and interfaces)
26
+ query = q(self.lang, """
27
+ (type_declaration
28
+ (type_spec
29
+ name: (type_identifier) @name
30
+ type: [
31
+ (struct_type)
32
+ (interface_type)
33
+ ] @type_body)) @definition
34
+ """)
35
+ definitions = []
36
+
37
+ all_methods = self._get_all_methods()
38
+
39
+ for _, captures in ts_get_captures(query, self._tree.root_node):
40
+ node = one(captures.get('definition'))
41
+ name_node = node.child_by_field_name("name") or node.named_child(0).child_by_field_name("name")
42
+
43
+ if not name_node:
44
+ continue
45
+
46
+ type_name = self._get_text(name_node)
47
+ type_body = node.named_child(0).child_by_field_name("type")
48
+ kind = "struct" if type_body.type == "struct_type" else "interface"
49
+
50
+ defn = Definition(name=type_name, kind=kind, **ts_line_info(node))
51
+ defn.methods = [m for m in all_methods if m._receiver_type == type_name]
52
+ definitions.append(defn)
53
+
54
+ return definitions
55
+
56
+ def iter_functions(self, include_calls=False) -> list[Function]:
57
+ """Returns top-level functions (those without receivers)."""
58
+ query = q(self.lang, """
59
+ (function_declaration
60
+ name: (identifier) @name
61
+ parameters: (parameter_list) @params) @func
62
+ """)
63
+
64
+ functions = []
65
+ for _, captures in ts_get_captures(query, self._tree.root_node):
66
+ func_node = captures.get('func')
67
+ name = self._get_text(one(captures.get('name')))
68
+ params = self._get_text(one(captures.get('params'))).strip("()")
69
+ functions.append(Function(name=name, arguments=params, **ts_line_info(func_node)))
70
+
71
+ return functions
72
+
73
+ def iter_globals(self) -> list[Variable]:
74
+ """Returns top-level var and const declarations."""
75
+ query = q(self.lang, """
76
+ (var_declaration
77
+ (var_spec name: (identifier) @name)) @var
78
+ (const_declaration
79
+ (const_spec name: (identifier) @name)) @const
80
+ """)
81
+
82
+ variables = []
83
+
84
+ for _, captures in ts_get_captures(query, self._tree.root_node):
85
+ # Simplification: grabbing the first identifier in the spec
86
+ node = captures.get('var') or captures.get('const')
87
+ name_node = node.named_child(0).child_by_field_name("name")
88
+ if name_node:
89
+ variables.append(Variable(name=self._get_text(name_node)))
90
+
91
+ return variables
92
+
93
+ def _get_all_methods(self) -> list:
94
+ """Helper to find all method_declarations and identify their receiver type."""
95
+ query = q(self.lang, """
96
+ (method_declaration
97
+ receiver: (parameter_list
98
+ (parameter_declaration
99
+ type: [
100
+ (pointer_type (type_identifier) @recv)
101
+ (type_identifier) @recv
102
+ ]
103
+ )
104
+ )
105
+ name: (field_identifier) @name
106
+ parameters: (parameter_list) @params
107
+ ) @method
108
+ """)
109
+
110
+ methods = []
111
+ for _, captures in ts_get_captures(query, self._tree.root_node):
112
+ method_node = one(captures.get('method'))
113
+ name_node = one(captures.get('name'))
114
+ recv_node = one(captures.get('recv'))
115
+ param_node = one(captures.get('params'))
116
+
117
+ func = Function(
118
+ name=self._get_text(name_node),
119
+ arguments=self._get_text(param_node).strip("()"),
120
+ **ts_line_info(method_node)
121
+ )
122
+ # Temporary attribute to help iter_definitions link them
123
+ func._receiver_type = self._get_text(recv_node)
124
+ methods.append(func)
125
+
126
+ return methods
@@ -0,0 +1,157 @@
1
+ from .base import Variable, Function, Definition, SourceFile
2
+ from tree_sitter import Language, Parser
3
+ import tree_sitter_javascript as tsjavascript
4
+ from .tree_utils import ts_get_captures, ts_line_info, one, q
5
+
6
+ def get_child_by_type(node, node_type):
7
+ for child in node.children:
8
+ if child.type == node_type:
9
+ return child
10
+ return None
11
+
12
+ class JsSourceFile(SourceFile):
13
+ lang = Language(tsjavascript.language())
14
+
15
+ def _parse(self):
16
+ self.parser = Parser(self.lang)
17
+ self._tree = self.parser.parse(self.code)
18
+ return self._tree
19
+
20
+ def _get_text(self, node):
21
+ if node is None:
22
+ return ''
23
+ return self.code[node.start_byte:node.end_byte].decode('utf-8')
24
+
25
+ def get_line_count(self):
26
+ """Fetch total lines in source file"""
27
+ return ts_line_info(self._tree.root_node)['source_lines'][1]
28
+
29
+ def iter_functions(self, include_calls=False) -> list[Function]:
30
+ """
31
+
32
+ Use case 1: Arrow functions:
33
+
34
+ query:
35
+ (lexical_declaration
36
+ (variable_declarator
37
+ value: (arrow_function)
38
+ )
39
+ ) @arrowfunc1
40
+ example:
41
+ const foobar = () => {
42
+ console.log('im an arrow function')
43
+ }
44
+
45
+ Use case 2: actual functions
46
+
47
+ query:
48
+ (function_declaration) @func
49
+ example:
50
+ function applyDiscount(p) {
51
+ return p * 0.9;
52
+ }
53
+
54
+ Use case 3: anon functions
55
+
56
+ query:
57
+ (lexical_declaration
58
+ (variable_declarator
59
+ value: (function_expression)
60
+ )
61
+ ) @anonFunc
62
+ example:
63
+ const anonymousExpress = function(a, b) {
64
+ return a + b;
65
+ };
66
+
67
+
68
+
69
+ """
70
+ query = q(self.lang, """
71
+ (program [
72
+ ;; 1. Standard or Exported Declaration
73
+ (function_declaration
74
+ name: (identifier) @name
75
+ parameters: (formal_parameters) @params) @anchor
76
+ (export_statement
77
+ (function_declaration
78
+ name: (identifier) @name
79
+ parameters: (formal_parameters) @params)) @anchor
80
+
81
+ ;; 2. Lexical Assignments (Arrow or Expression)
82
+ (lexical_declaration
83
+ (variable_declarator
84
+ name: (identifier) @name
85
+ value: [
86
+ (function_expression parameters: (formal_parameters) @params)
87
+ (arrow_function parameters: (formal_parameters) @params)
88
+ ]
89
+ )
90
+ ) @anchor
91
+ ])
92
+ """)
93
+ functions = []
94
+ for _, captures in ts_get_captures(query, self._tree.root_node):
95
+
96
+ name_node = one(captures.get('name', [None]))
97
+ param_node = one(captures.get('params', [None]))
98
+ anchor_node = one(captures.get('anchor', [None]))
99
+
100
+ if name_node and param_node:
101
+ name = self._get_text(name_node)
102
+ args = self._get_text(param_node).strip("()")
103
+
104
+ f = Function(name=name, arguments=args, **ts_line_info(anchor_node))
105
+ if include_calls and anchor_node:
106
+ f.calls = self._extract_calls(anchor_node)
107
+ functions.append(f)
108
+
109
+ return functions
110
+
111
+ def iter_definitions(self, include_methods=False, include_calls=False) -> list[Definition]:
112
+ query = q(self.lang, """
113
+ (class_declaration
114
+ name: (identifier) @name
115
+ body: (class_body) @body) @class
116
+ """)
117
+
118
+ definitions = []
119
+
120
+ for _, captures in ts_get_captures(query, self._tree.root_node):
121
+ name_node = one(captures.get('name'))
122
+ class_node = one(captures.get('class'))
123
+ class_name = self._get_text(name_node)
124
+ defn = Definition(name=class_name, kind="class", **ts_line_info(class_node))
125
+
126
+ if not include_methods:
127
+ definitions.append(defn)
128
+ continue
129
+
130
+ body_node = class_node.child_by_field_name("body")
131
+ # In JS class_body, we look for method_definition
132
+ for child in body_node.children:
133
+ if child.type == "method_definition":
134
+ name_node = child.child_by_field_name("name")
135
+ param_node = child.child_by_field_name("parameters")
136
+
137
+ method = Function(
138
+ name=self._get_text(name_node),
139
+ arguments=self._get_text(param_node).strip("()"),
140
+ **ts_line_info()
141
+ )
142
+ if include_calls:
143
+ method.calls = self._extract_calls(child)
144
+ defn.methods.append(method)
145
+
146
+ definitions.append(defn)
147
+ return definitions
148
+
149
+ def iter_globals(self):
150
+ query = q(self.lang, """
151
+ (program [
152
+ (lexical_declaration (variable_declarator name: (identifier) @name))
153
+ (variable_declaration (variable_declarator name: (identifier) @name))
154
+ ] @global)
155
+ """)
156
+ captures = query.captures(self._tree.root_node)
157
+ return [self._get_text(node.child_by_field_name("name")) for node in captures.get('global', [])]
@@ -0,0 +1,89 @@
1
+ import tree_sitter_python as tspython
2
+ from tree_sitter import Language, Parser
3
+ from .base import SourceFile, Function, Variable, Definition
4
+ from .tree_utils import ts_get_captures,ts_line_info, one, q
5
+
6
+
7
+ def extract_decorated_function(node):
8
+ for child in node.children:
9
+ if child.type == 'function_definition':
10
+ return child
11
+ return None
12
+
13
+
14
+ class PythonSourceFile(SourceFile):
15
+ lang = Language(tspython.language())
16
+
17
+ def _parse(self):
18
+ self.parser = Parser(self.lang)
19
+ return self.parser.parse(self.code)
20
+
21
+ def _get_text(self, node):
22
+ if node is None:
23
+ return ''
24
+ return self.code[node.start_byte:node.end_byte].decode('utf-8')
25
+
26
+ def get_line_count(self):
27
+ """Fetch total lines in source file"""
28
+ return ts_line_info(self._tree.root_node)['source_lines'][1]
29
+
30
+ def iter_functions(self, include_calls=False) -> list[Function]:
31
+ query = q(self.lang, """
32
+ (module (function_definition
33
+ name: (identifier) @name
34
+ parameters: (parameters) @params) @func)
35
+ """)
36
+ functions = []
37
+ for _, captures in ts_get_captures(query, self._tree.root_node):
38
+ func_node = one(captures.get('func'))
39
+ name_node = one(captures.get("name"))
40
+ param_node = one(captures.get("params"))
41
+
42
+ if name_node:
43
+ name = self._get_text(name_node)
44
+ params = self._get_text(param_node).strip("()")
45
+
46
+ f = Function(name=name, arguments=params, **ts_line_info(func_node))
47
+ if include_calls:
48
+ f.calls = self._extract_calls(func_node)
49
+ functions.append(f)
50
+
51
+ return functions
52
+
53
+ def iter_definitions(self, include_methods=False, include_calls=False) -> list[Definition]:
54
+ # Query for the class and its internal methods
55
+ query = q(self.lang, """
56
+ (class_definition
57
+ name: (identifier) @name
58
+ body: (block) @body) @class
59
+ """)
60
+ definitions = []
61
+ for _, captures in ts_get_captures(query, self._tree.root_node):
62
+ class_name = self._get_text(captures.get('name')[0])
63
+ body_node = one(captures.get('body'))
64
+ class_node = one(captures.get('class'))
65
+
66
+ # Initialize the Definition
67
+ defn = Definition(name=class_name, kind="class", **ts_line_info(class_node))
68
+
69
+ if not include_methods:
70
+ continue
71
+ # Walk the class body to find function_definitions (Methods)
72
+ for child in body_node.children:
73
+ entry = child
74
+ if entry.type == "decorated_definition":
75
+ decorated_function = extract_decorated_function(entry)
76
+ if decorated_function is not None:
77
+ entry = decorated_function
78
+ if entry.type == "function_definition":
79
+ method_name = self._get_text(entry.child_by_field_name("name"))
80
+ params = self._get_text(entry.child_by_field_name("parameters")).strip("()")
81
+ method = Function(name=method_name, arguments=params, **ts_line_info(child))
82
+ if include_calls:
83
+ method.calls = self._extract_calls(entry)
84
+ defn.methods.append(method)
85
+ definitions.append(defn)
86
+ return definitions
87
+
88
+ def iter_globals(self):
89
+ raise NotImplemented()
@@ -0,0 +1,68 @@
1
+ from typing import Dict, Callable, Type
2
+ from .base import SourceFile
3
+
4
+ import sys
5
+
6
+ def _load_python():
7
+ from .handler_py import PythonSourceFile
8
+ return PythonSourceFile
9
+
10
+ def _load_go():
11
+ from .handler_go import GoSourceFile
12
+ return GoSourceFile
13
+
14
+ def _load_js():
15
+ from .handler_js import JsSourceFile
16
+ return JsSourceFile
17
+
18
+ class HandlerRegistry:
19
+ LIBRARIES = {
20
+ "py": {
21
+ 'package_name': 'tree-sitter-python',
22
+ 'lib': 'tree_sitter_python',
23
+ 'loader': _load_python,
24
+ },
25
+ "go": {
26
+ 'package_name': 'tree-sitter-go',
27
+ 'lib': 'tree_sitter_go',
28
+ 'loader': _load_go,
29
+ },
30
+ "js": {
31
+ 'package_name': 'tree-sitter-javascript',
32
+ 'lib': 'tree_sitter_javascript',
33
+ 'loader': _load_js,
34
+ },
35
+ }
36
+
37
+ def __init__(self):
38
+ # Maps extension -> A function that returns the Class
39
+ self._loaders = {
40
+ ".py": self.LIBRARIES['py']['loader'],
41
+ ".go": self.LIBRARIES['go']['loader'],
42
+ ".js": self.LIBRARIES['js']['loader'],
43
+ }
44
+ self._handlers: Dict[str, Type[SourceFile]] = {}
45
+ self._errors = {}
46
+
47
+ def get_handler_class(self, extension: str) -> Type[SourceFile]:
48
+ ext = extension.lower()
49
+ if not self.has_handler(ext):
50
+ raise ValueError(f"Unsupported extension: {ext}")
51
+
52
+ # Check cache so we don't re-import every time
53
+ if ext not in self._handlers:
54
+ loader_func = self._loaders[ext]
55
+ try:
56
+ self._handlers[ext] = loader_func()
57
+ except ModuleNotFoundError as e:
58
+ sys.stderr.write(f"Skipping files of type {ext} due to import failure for required module. Error: {str(e)}\n")
59
+ self._errors[ext] = e
60
+ return None
61
+
62
+ return self._handlers[ext]
63
+
64
+ def has_handler(self, extension: str) -> bool:
65
+ ext = extension.lower()
66
+ if ext in self._errors:
67
+ return False
68
+ return ext in self._loaders or ext in self._handlers
@@ -0,0 +1,48 @@
1
+ try:
2
+ from tree_sitter import QueryCursor, Query
3
+ except (ImportError, AttributeError):
4
+ QueryCursor = None
5
+ Query = None
6
+
7
+ def ts_line_info(node):
8
+ if isinstance(node, list) and len(node) == 1:
9
+ node = node[0]
10
+ return {'source_lines': (node.start_point[0], node.end_point[0])}
11
+
12
+ def ts_get_captures(query, root_node):
13
+ """
14
+ A cross-version generator for tree-sitter captures.
15
+ """
16
+ # Version A: Modern API (0.21.0+)
17
+ if QueryCursor is not None:
18
+ cursor = QueryCursor(query)
19
+ for index, captures in cursor.matches(root_node):
20
+ yield index, captures
21
+ return
22
+
23
+ # Version B: Older versions dont have cursor and allow query.captures(root_node) which returns a list
24
+ if hasattr(query, 'captures'):
25
+ results = query.matches(root_node)
26
+ for index, captures in results:
27
+ yield index, captures
28
+ else:
29
+ raise RuntimeError("No compatible tree-sitter capture method found.")
30
+
31
+ def one(source):
32
+ """ helper that extracts the first element or child for a tree node or None """
33
+ if source is None:
34
+ return None
35
+ if hasattr(source, "children"):
36
+ source = source.children
37
+ if isinstance(source, list):
38
+ if len(source) >= 1:
39
+ return source[0]
40
+ else:
41
+ return None
42
+ raise ValueError("Unsupported type sent to one()")
43
+
44
+ def q(lang, text):
45
+ if Query is not None:
46
+ return Query(lang, text)
47
+ return lang.query(text)
48
+
@@ -0,0 +1,72 @@
1
+ def as_sorted_dict(d):
2
+ return dict(sorted(d.items()))
3
+
4
+ def line_count_pad_size(source):
5
+ m = 0
6
+ for report in source.values():
7
+ m = max(m, report['lines'])
8
+ return (len(str(m)) * 2) + len('[:]')
9
+
10
+ def get_line_range_str(item=None, size=None, default=''):
11
+ if size == 0:
12
+ lines = (0, 0)
13
+ elif size is not None:
14
+ lines = (1, size)
15
+ elif hasattr(item, 'source_lines'):
16
+ lines = (item.source_lines[0], item.source_lines[1])
17
+ else:
18
+ return default
19
+
20
+ range_text = f"[{lines[0]}:{lines[1]}]"
21
+ return f"{range_text}"
22
+
23
+
24
+ def display_txt(reports_by_file, output, lines=False):
25
+ if lines:
26
+ max_gutter = line_count_pad_size(reports_by_file) + 1 # for padding
27
+ gutter_spacing = " " * max_gutter
28
+ else:
29
+ max_gutter = 0
30
+ gutter_spacing = ''
31
+ line_range = ''
32
+
33
+ for file_path, report in as_sorted_dict(reports_by_file).items():
34
+ if lines:
35
+ file_lines = get_line_range_str(size=report['lines']).ljust(max_gutter)
36
+ else:
37
+ file_lines = ''
38
+ output.write(f"\n{file_lines}\033[1m # {file_path}\033[0m\n")
39
+
40
+ all_top_level = ([(f, 'func') for f in sorted(report.get('functions', []), key=lambda x: x.name)] +
41
+ [(d, 'def') for d in sorted(report.get('definitions', []), key=lambda x: x.name)])
42
+
43
+ for i, (item, kind) in enumerate(all_top_level):
44
+ is_last_top = (i == len(all_top_level) - 1)
45
+ marker = "└── " if is_last_top else "├── "
46
+ pipe = " " if is_last_top else "│ "
47
+
48
+ if lines:
49
+ line_range = get_line_range_str(item, default=gutter_spacing).ljust(max_gutter)
50
+
51
+ if kind == 'func':
52
+ output.write(f"{line_range} {marker} {item}\n")
53
+ # Render Calls
54
+ for j, call in enumerate(sorted(item.calls)):
55
+ is_last_call = (j == len(item.calls) - 1)
56
+ c_marker = "└── ➜ " if is_last_call else "├── ➜ "
57
+ output.write(f"{gutter_spacing} {pipe}{c_marker}{call}\n")
58
+
59
+ elif kind == 'def':
60
+ output.write(f"{line_range} {marker}○ {item}\n")
61
+ # Render Methods
62
+ methods = item.methods
63
+ for j, method in enumerate(methods):
64
+ is_last_method = (j == len(methods) - 1)
65
+ m_marker = "└── " if is_last_method else "├── "
66
+ m_pipe = " " if is_last_method else "│ "
67
+
68
+ if lines:
69
+ m_line_range = get_line_range_str(method, default=gutter_spacing).ljust(max_gutter)
70
+ else:
71
+ m_line_range = ''
72
+ output.write(f"{m_line_range} {pipe}{m_marker}{method}\n")
@@ -0,0 +1,119 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Generator, Tuple, Optional, Set
4
+
5
+ import zipfile
6
+ from dataclasses import dataclass
7
+ from typing import Callable
8
+
9
+ @dataclass
10
+ class FileEntry:
11
+ full_path: Path # The logical path within the project
12
+ rel_path: Path
13
+ size: int # For your max_bytes check
14
+ reader: Callable[[], bytes] # The "make_reader" logic
15
+
16
+ class DiskSource:
17
+ def __init__(self, root_path: Path):
18
+ self.path = Path(root_path)
19
+ self.is_single_file = self.path.is_file()
20
+ self.root = self.path.parent if self.is_single_file else self.path
21
+
22
+ def walk(self) -> Generator[FileEntry, None, None]:
23
+ if self.is_single_file:
24
+ yield FileEntry(
25
+ full_path=self.path,
26
+ rel_path=Path(self.path.name),
27
+ size=self.path.stat().st_size,
28
+ reader=self.path.read_bytes
29
+ )
30
+ return
31
+
32
+ for root, dirs, files in os.walk(self.root):
33
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ProjectScanner.DEFAULT_IGNORE_LIST]
34
+ for f in files:
35
+ p = Path(root) / f
36
+ yield FileEntry(
37
+ full_path=p,
38
+ rel_path=p.relative_to(self.root),
39
+ size=p.stat().st_size,
40
+ reader=p.read_bytes
41
+ )
42
+
43
+ class ZipSource:
44
+ def __init__(self, buffer):
45
+ self.zip = zipfile.ZipFile(buffer)
46
+
47
+ @classmethod
48
+ def from_path(cls, path):
49
+ # Open file is seekable
50
+ return cls(open(path, 'rb'))
51
+
52
+ @classmethod
53
+ def from_stream(cls, stream):
54
+ # Pipes aren't seekable, so we must read the whole thing into memory
55
+ # to allow ZipFile to jump to the Central Directory at the end.
56
+ seekable_buffer = io.BytesIO(stream.read())
57
+ return cls(seekable_buffer)
58
+
59
+ def walk(self) -> Generator[FileEntry, None, None]:
60
+ for info in self.zip.infolist():
61
+ if info.is_dir():
62
+ continue
63
+
64
+ rel_path = Path(info.filename)
65
+ yield FileEntry(
66
+ full_path=rel_path,
67
+ rel_path=rel_path,
68
+ size=info.file_size,
69
+ reader=lambda name=info.filename: self.zip.read(name)
70
+ )
71
+
72
+
73
+ class ProjectScanner:
74
+ DEFAULT_IGNORE_LIST = {
75
+ "node_modules", "bower_components", "vendor",
76
+ "dist", "build", "out", "venv", "env", "target"
77
+ }
78
+
79
+ def __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None):
80
+ self.registry = registry
81
+ self.max_bytes = max_bytes
82
+ self.ignored_segments = self.DEFAULT_IGNORE_LIST
83
+ if extra_ignored_dirs:
84
+ self.ignored_segments.update(extra_ignored_dirs)
85
+
86
+ def is_visible(self, entry: FileEntry) -> bool:
87
+ if entry.size > self.max_bytes:
88
+ return False
89
+ if not self.registry.has_handler(entry.rel_path.suffix):
90
+ return False
91
+ for part in entry.rel_path.parts:
92
+ if part.startswith(".") or part in self.ignored_segments:
93
+ return False
94
+ return True
95
+
96
+ def make_reader(self, path):
97
+ def reader():
98
+ with open(path, 'rb') as f:
99
+ content = f.read()
100
+ return content
101
+ return reader
102
+
103
+ def scan(self, source) -> Generator[Tuple[Path, Path, type, Callable], None, None]:
104
+ """
105
+ Yields (Full_Path, Relative_Path, Handler, Reader)
106
+ """
107
+ for entry in source.walk():
108
+ # Check visibility based on the relative path (to catch ignored folders)
109
+ if not self.is_visible(entry):
110
+ continue
111
+
112
+ handler_class = self.registry.get_handler_class(entry.rel_path.suffix)
113
+ if handler_class:
114
+ yield (
115
+ entry.full_path,
116
+ entry.rel_path,
117
+ handler_class,
118
+ entry.reader
119
+ )
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: radixcodemap
3
+ Version: 0.2.0
4
+ Summary: Summarize code repositories quickly and (soon) with multiple verbosity levels
5
+ Home-page: https://github.com/jdotpy/radix-map
6
+ Download-URL: https://github.com/jdotpy/radix-map/tarball/master
7
+ Author: KJ
8
+ Author-email: jdotpy@users.noreply.github.com
9
+ Keywords: tools
10
+ Classifier: Programming Language :: Python :: 3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: tree-sitter
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Requires-Dist: twine; extra == "dev"
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: download-url
23
+ Dynamic: home-page
24
+ Dynamic: keywords
25
+ Dynamic: license-file
26
+ Dynamic: provides-extra
27
+ Dynamic: requires-dist
28
+ Dynamic: summary
29
+
30
+
31
+
32
+ # Radix: Configurable SourceCode summarizer
33
+
34
+ Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
35
+
36
+
37
+ ## Installation
38
+ ---
39
+
40
+ 1. **Clone the repository:**
41
+ ```bash
42
+ git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
43
+ cd radix-map
44
+ pip install .
45
+ ```
46
+
47
+
48
+ 2. **Install Dependencies based on code you plan on using:**
49
+ ```bash
50
+ pip install tree-sitter-python
51
+ pip install tree-sitter-go
52
+ pip install tree-sitter-javascript
53
+ ```
54
+
55
+
56
+ ## Use
57
+ ---
58
+
59
+ ```bash
60
+ radix map .
61
+ ```
62
+
63
+ Example output:
64
+ ```bash
65
+ #tests/test_integration.py
66
+ └── ƒ get_test_pairs()
67
+
68
+ #tests/snapshots/python_ex1.py
69
+ ├── ƒ global_helper()
70
+ └── ○ class DataProcessor
71
+ ├── ƒ __init__(self, source: str)
72
+ ├── ƒ process(self)
73
+ └── ƒ _validate(self)
74
+
75
+ #radix/scanner.py
76
+ └── ○ class ProjectScanner
77
+ ├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
78
+ ├── ƒ is_visible(self, path: Path)
79
+ └── ƒ scan(self, target: str)
80
+ ```
81
+
82
+ ## Supported Languages
83
+
84
+
85
+ | Language | Status | Package Requirement |
86
+ |---|---|---|
87
+ | Python | ✅ functions & classes | tree-sitter-python |
88
+ | Go | 🚧 | tree-sitter-go |
89
+ | JavaScript | 🚧 | tree-sitter-javascript |
90
+
91
+ ```
92
+
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ radix/__init__.py
5
+ radix/__main__.py
6
+ radix/cli.py
7
+ radix/core.py
8
+ radix/report.py
9
+ radix/scanner.py
10
+ radix/handlers/__init__.py
11
+ radix/handlers/base.py
12
+ radix/handlers/handler_go.py
13
+ radix/handlers/handler_js.py
14
+ radix/handlers/handler_py.py
15
+ radix/handlers/registry.py
16
+ radix/handlers/tree_utils.py
17
+ radixcodemap.egg-info/PKG-INFO
18
+ radixcodemap.egg-info/SOURCES.txt
19
+ radixcodemap.egg-info/dependency_links.txt
20
+ radixcodemap.egg-info/entry_points.txt
21
+ radixcodemap.egg-info/requires.txt
22
+ radixcodemap.egg-info/top_level.txt
23
+ tests/test_integration.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ radix = radix.__main__:entrypoint
@@ -0,0 +1,5 @@
1
+ tree-sitter
2
+
3
+ [dev]
4
+ pytest
5
+ twine
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from setuptools import setup
2
+
3
+ with open("README.md", "r") as f:
4
+ long_description = f.read()
5
+
6
+ setup(
7
+ name = 'radixcodemap',
8
+ entry_points={
9
+ 'console_scripts': [
10
+ 'radix=radix.__main__:entrypoint',
11
+ ],
12
+ },
13
+ packages=['radix', 'radix.handlers'],
14
+ version = '0.2.0',
15
+ description = 'Summarize code repositories quickly and (soon) with multiple verbosity levels',
16
+ long_description=long_description,
17
+ long_description_content_type="text/markdown",
18
+ author = 'KJ',
19
+ author_email = 'jdotpy@users.noreply.github.com',
20
+ url = 'https://github.com/jdotpy/radix-map',
21
+ download_url = 'https://github.com/jdotpy/radix-map/tarball/master',
22
+ keywords = ['tools'],
23
+ install_requires=['tree-sitter'],
24
+ extras_require={
25
+ 'dev': ['pytest', 'twine'],
26
+ },
27
+ classifiers = [
28
+ "Programming Language :: Python :: 3",
29
+ ],
30
+ )
@@ -0,0 +1,49 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from radix.core import analyze_project
4
+ from radix.report import display_txt
5
+ from radix.handlers.registry import HandlerRegistry
6
+ from .utils import MockScanner, MockSource
7
+
8
+ import io
9
+
10
+ # Setup paths
11
+ TEST_DATA_DIR = Path(__file__).parent / "snapshots"
12
+ SNAPSHOT_SUFFIX = '.snapshot'
13
+
14
+ def get_test_pairs():
15
+ """
16
+ Finds all source files and pairs them with their corresponding .snapshot output.
17
+ Returns a list of tuples: (source_path, expected_output_path)
18
+ """
19
+ pairs = []
20
+ for snapshot_file in TEST_DATA_DIR.glob("*" + SNAPSHOT_SUFFIX):
21
+ # Matches 'python_ex1.py' with 'python_ex1.py.snapshot'
22
+ source_file = TEST_DATA_DIR / str(snapshot_file)[:-len(SNAPSHOT_SUFFIX)]
23
+ if source_file.exists():
24
+ pairs.append((source_file, snapshot_file))
25
+ return pairs
26
+
27
+ @pytest.mark.parametrize("source_path, expected_path", get_test_pairs())
28
+ def test_report_generation(source_path, expected_path):
29
+ source_code = source_path.read_bytes()
30
+ expected_output = expected_path.read_text()
31
+
32
+ virtual_path = source_path.name
33
+ handler = HandlerRegistry().get_handler_class(source_path.suffix)
34
+
35
+ source = MockSource([])
36
+ scanner = MockScanner(None, [
37
+ (
38
+ virtual_path,
39
+ virtual_path,
40
+ handler,
41
+ lambda: source_code,
42
+ )
43
+ ])
44
+
45
+ test_output = io.StringIO()
46
+ reports_by_file = analyze_project(scanner, source)
47
+ display_txt(reports_by_file, test_output)
48
+ result = test_output.getvalue()
49
+ assert result == expected_output, f"Snapshot comparison failed for {source_path.name}"