codemap-python 0.1.2__tar.gz → 0.1.4__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.
Files changed (77) hide show
  1. {codemap_python-0.1.2 → codemap_python-0.1.4}/PKG-INFO +24 -12
  2. {codemap_python-0.1.2 → codemap_python-0.1.4}/README.md +22 -9
  3. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/call_extractor.py +7 -6
  4. codemap_python-0.1.4/analysis/core/ast_parser.py +25 -0
  5. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/core/import_extractor.py +7 -5
  6. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/explain_runner.py +6 -6
  7. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/runners/phase4_runner.py +6 -6
  8. codemap_python-0.1.4/analysis/utils/bom_handler.py +119 -0
  9. codemap_python-0.1.4/analysis/utils/progress_spinner.py +85 -0
  10. {codemap_python-0.1.2 → codemap_python-0.1.4}/cli.py +7 -0
  11. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/PKG-INFO +24 -12
  12. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/SOURCES.txt +3 -0
  13. {codemap_python-0.1.2 → codemap_python-0.1.4}/pyproject.toml +2 -3
  14. codemap_python-0.1.4/tests/test_cli_invalid_escape_warnings.py +35 -0
  15. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/app.py +5 -1
  16. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/static/app.js +63 -4
  17. codemap_python-0.1.2/analysis/core/ast_parser.py +0 -8
  18. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/__init__.py +0 -0
  19. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/architecture/__init__.py +0 -0
  20. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/architecture/architecture_engine.py +0 -0
  21. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/architecture/dependency_cycles.py +0 -0
  22. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/architecture/risk_radar.py +0 -0
  23. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/__init__.py +0 -0
  24. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/call_graph_builder.py +0 -0
  25. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/call_resolver.py +0 -0
  26. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/context_models.py +0 -0
  27. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/cross_file_resolver.py +0 -0
  28. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/execution_tracker.py +0 -0
  29. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/flow_builder.py +0 -0
  30. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/call_graph/models.py +0 -0
  31. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/core/__init__.py +0 -0
  32. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/core/ast_context.py +0 -0
  33. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/core/class_extractor.py +0 -0
  34. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/core/function_extractor.py +0 -0
  35. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/__init__.py +0 -0
  36. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/docstring_extractor.py +0 -0
  37. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/repo_summary_generator.py +0 -0
  38. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/return_analyzer.py +0 -0
  39. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/risk_flags.py +0 -0
  40. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/signature_extractor.py +0 -0
  41. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/explain/summary_generator.py +0 -0
  42. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/graph/__init__.py +0 -0
  43. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/graph/callgraph_index.py +0 -0
  44. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/graph/entrypoint_detector.py +0 -0
  45. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/graph/impact_analyzer.py +0 -0
  46. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/indexing/__init__.py +0 -0
  47. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/indexing/import_resolver.py +0 -0
  48. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/indexing/symbol_index.py +0 -0
  49. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/runners/__init__.py +0 -0
  50. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/utils/__init__.py +0 -0
  51. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/utils/ast_helpers.py +0 -0
  52. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/utils/cache_manager.py +0 -0
  53. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/utils/path_resolver.py +0 -0
  54. {codemap_python-0.1.2 → codemap_python-0.1.4}/analysis/utils/repo_fetcher.py +0 -0
  55. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_cli.py +0 -0
  56. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/dependency_links.txt +0 -0
  57. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/entry_points.txt +0 -0
  58. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/requires.txt +0 -0
  59. {codemap_python-0.1.2 → codemap_python-0.1.4}/codemap_python.egg-info/top_level.txt +0 -0
  60. {codemap_python-0.1.2 → codemap_python-0.1.4}/security_utils.py +0 -0
  61. {codemap_python-0.1.2 → codemap_python-0.1.4}/setup.cfg +0 -0
  62. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_cache_cli_commands.py +0 -0
  63. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_cache_retention.py +0 -0
  64. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_no_key_persistence.py +0 -0
  65. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_registry_session_mode.py +0 -0
  66. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_security_cli_integration.py +0 -0
  67. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_security_redaction.py +0 -0
  68. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_symbol_explain_cache.py +0 -0
  69. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_symbol_info_endpoint.py +0 -0
  70. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_ui_private_mode_security.py +0 -0
  71. {codemap_python-0.1.2 → codemap_python-0.1.4}/tests/test_ui_retention_controls.py +0 -0
  72. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/__init__.py +0 -0
  73. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/device_id.py +0 -0
  74. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/static/styles.css +0 -0
  75. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/templates/index.html +0 -0
  76. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/utils/__init__.py +0 -0
  77. {codemap_python-0.1.2 → codemap_python-0.1.4}/ui/utils/registry_manager.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codemap-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Local Python code analysis tool - understand architecture, dependencies, and call graphs
5
5
  Author-email: ADITYA <aditykushwaha69@gmail.com>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/ADITYA-kus/codemap_ai
8
8
  Project-URL: Repository, https://github.com/ADITYA-kus/codemap_ai.git
9
9
  Project-URL: Issues, https://github.com/ADITYA-kus/codemap_ai/issues
@@ -11,7 +11,6 @@ Project-URL: Documentation, https://github.com/ADITYA-kus/codemap_ai#readme
11
11
  Keywords: code-analysis,python,architecture,call-graph,cli,dashboard,local,privacy
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
@@ -159,22 +158,35 @@ codemap open --port 8000
159
158
 
160
159
  ### Cache Management
161
160
  ```bash
162
- # List all analyzed repositories
161
+ # 📋 List all analyzed repositories and their cache info
163
162
  codemap cache list
164
163
 
165
- # Show cache details for a repository
166
- codemap cache info <repo_hash>
164
+ # 📊 Show detailed cache information for a specific repository
165
+ codemap cache info --path <repo_directory>
167
166
 
168
- # Clear a specific repository's cache
169
- codemap cache clear <repo_hash>
167
+ # ⏱️ Set cache retention policy (automatically clean old caches)
168
+ codemap cache retention --path <repo_directory> --days 30 --yes
170
169
 
171
- # Show cache retention policy
172
- codemap cache retention <repo_hash>
170
+ # 🧹 Preview what would be cleaned (safe, no deletion)
171
+ codemap cache sweep --dry-run
173
172
 
174
- # Sweep expired caches (auto-cleanup)
175
- codemap cache sweep
173
+ # 🧹 Actually clean up expired caches (requires --yes confirmation)
174
+ codemap cache sweep --yes
175
+
176
+ # 🗑️ Clear cache for a specific repository (preview first)
177
+ codemap cache clear --path <repo_directory> --dry-run
178
+
179
+ # 🗑️ Actually delete a repository's cache (requires --yes confirmation)
180
+ codemap cache clear --path <repo_directory> --yes
176
181
  ```
177
182
 
183
+ **Cache Management Tips:**
184
+ - ✅ Always use `--dry-run` first to preview changes
185
+ - ✅ Add `--yes` flag to skip confirmation (useful in scripts)
186
+ - ✅ Default retention is 14 days; adjust with `--days <number>`
187
+ - ✅ Cache is stored in: `~/.codemap_cache/` (varies by OS)
188
+ - ✅ Use `cache list` to see all cached repositories and their sizes
189
+
178
190
  **Get GitHub Token (for private repos):**
179
191
  1. Go to https://github.com/settings/tokens
180
192
  2. Click "Generate new token" → "Generate new token (classic)"
@@ -131,22 +131,35 @@ codemap open --port 8000
131
131
 
132
132
  ### Cache Management
133
133
  ```bash
134
- # List all analyzed repositories
134
+ # 📋 List all analyzed repositories and their cache info
135
135
  codemap cache list
136
136
 
137
- # Show cache details for a repository
138
- codemap cache info <repo_hash>
137
+ # 📊 Show detailed cache information for a specific repository
138
+ codemap cache info --path <repo_directory>
139
139
 
140
- # Clear a specific repository's cache
141
- codemap cache clear <repo_hash>
140
+ # ⏱️ Set cache retention policy (automatically clean old caches)
141
+ codemap cache retention --path <repo_directory> --days 30 --yes
142
142
 
143
- # Show cache retention policy
144
- codemap cache retention <repo_hash>
143
+ # 🧹 Preview what would be cleaned (safe, no deletion)
144
+ codemap cache sweep --dry-run
145
145
 
146
- # Sweep expired caches (auto-cleanup)
147
- codemap cache sweep
146
+ # 🧹 Actually clean up expired caches (requires --yes confirmation)
147
+ codemap cache sweep --yes
148
+
149
+ # 🗑️ Clear cache for a specific repository (preview first)
150
+ codemap cache clear --path <repo_directory> --dry-run
151
+
152
+ # 🗑️ Actually delete a repository's cache (requires --yes confirmation)
153
+ codemap cache clear --path <repo_directory> --yes
148
154
  ```
149
155
 
156
+ **Cache Management Tips:**
157
+ - ✅ Always use `--dry-run` first to preview changes
158
+ - ✅ Add `--yes` flag to skip confirmation (useful in scripts)
159
+ - ✅ Default retention is 14 days; adjust with `--days <number>`
160
+ - ✅ Cache is stored in: `~/.codemap_cache/` (varies by OS)
161
+ - ✅ Use `cache list` to see all cached repositories and their sizes
162
+
150
163
  **Get GitHub Token (for private repos):**
151
164
  1. Go to https://github.com/settings/tokens
152
165
  2. Click "Generate new token" → "Generate new token (classic)"
@@ -1,6 +1,7 @@
1
- # AST Call detection
2
-
3
- import ast
1
+ # AST Call detection
2
+
3
+ import ast
4
+ from analysis.utils.bom_handler import read_source_file, parse_source_to_ast
4
5
 
5
6
  class FunctionCallVisitor(ast.NodeVisitor):
6
7
  def __init__(self, file_path):
@@ -81,9 +82,9 @@ class FunctionCallVisitor(ast.NodeVisitor):
81
82
  return None
82
83
 
83
84
 
84
- def extract_function_calls(file_path):
85
- with open(file_path, "r", encoding="utf-8") as f:
86
- tree = ast.parse(f.read())
85
+ def extract_function_calls(file_path):
86
+ source = read_source_file(file_path)
87
+ tree = parse_source_to_ast(source, file_path=file_path)
87
88
 
88
89
  visitor = FunctionCallVisitor(file_path)
89
90
  visitor.visit(tree)
@@ -0,0 +1,25 @@
1
+ # AST Parser Module
2
+ from analysis.utils.bom_handler import read_and_parse_python_file
3
+
4
+
5
+ def parse_python_file(file_path):
6
+ """Parse a Python file with automatic encoding and BOM handling.
7
+
8
+ This function:
9
+ 1. Reads the file with automatic encoding detection (UTF-8 → Latin-1)
10
+ 2. Removes any BOM characters automatically
11
+ 3. Parses the cleaned source code
12
+
13
+ Args:
14
+ file_path: Path to Python file to parse
15
+
16
+ Returns:
17
+ ast.Module: Parsed AST tree
18
+
19
+ Raises:
20
+ SyntaxError: If source code has syntax errors
21
+ FileNotFoundError: If file doesn't exist
22
+ ValueError: If file encoding cannot be determined
23
+ """
24
+ return read_and_parse_python_file(file_path)
25
+
@@ -1,13 +1,15 @@
1
1
  # Import Extractor Module
2
2
  # analysis/import_extractor.py
3
3
 
4
- import ast
4
+ import ast
5
+ from analysis.utils.bom_handler import read_source_file, parse_source_to_ast
5
6
 
6
- def extract_imports(file_path):
7
- with open(file_path, "r", encoding="utf-8") as f:
8
- source = f.read()
9
7
 
10
- tree = ast.parse(source)
8
+ def extract_imports(file_path):
9
+ """Extract imports from a Python file with automatic encoding and BOM handling."""
10
+ source = read_source_file(file_path)
11
+ tree = parse_source_to_ast(source, file_path=file_path)
12
+
11
13
  imports = []
12
14
 
13
15
  for node in ast.walk(tree):
@@ -5,9 +5,8 @@ from __future__ import annotations
5
5
 
6
6
  from typing import Optional, Dict, Any
7
7
 
8
- import ast
9
- import json
10
- import os
8
+ import json
9
+ import os
11
10
 
12
11
  from analysis.indexing.symbol_index import SymbolIndex, SymbolInfo
13
12
  from analysis.graph.callgraph_index import CallGraphIndex, CallSite
@@ -26,9 +25,10 @@ def collect_python_files(root_dir: str):
26
25
  return py_files
27
26
 
28
27
 
29
- def parse_ast(file_path: str) -> ast.AST:
30
- with open(file_path, "r", encoding="utf-8") as f:
31
- return ast.parse(f.read())
28
+ def parse_ast(file_path: str):
29
+ """Parse a Python file with automatic encoding and BOM handling."""
30
+ from analysis.utils.bom_handler import read_and_parse_python_file
31
+ return read_and_parse_python_file(file_path)
32
32
 
33
33
 
34
34
  def file_to_module(file_path: str, repo_root: str) -> str:
@@ -3,9 +3,8 @@ from __future__ import annotations
3
3
 
4
4
  from typing import Optional, Dict, Any, List
5
5
 
6
- import os
7
- import ast
8
- import json
6
+ import os
7
+ import json
9
8
  from analysis.indexing.symbol_index import SymbolIndex
10
9
  from analysis.indexing.import_resolver import ImportResolver
11
10
  from analysis.call_graph.cross_file_resolver import CrossFileResolver
@@ -28,9 +27,10 @@ def collect_python_files(root_dir: str) -> List[str]:
28
27
  return py_files
29
28
 
30
29
 
31
- def parse_ast(file_path: str):
32
- with open(file_path, "r", encoding="utf-8") as f:
33
- return ast.parse(f.read())
30
+ def parse_ast(file_path: str):
31
+ """Parse a Python file, automatically handling encoding and UTF-8 BOM."""
32
+ from analysis.utils.bom_handler import read_and_parse_python_file
33
+ return read_and_parse_python_file(file_path)
34
34
 
35
35
 
36
36
  def file_to_module(file_path: str, repo_root: str) -> str:
@@ -0,0 +1,119 @@
1
+ """BOM (Byte Order Mark), encoding, and AST parsing utilities for CodeMap.
2
+
3
+ This module provides utilities to handle:
4
+ 1. UTF-8 BOM (Byte Order Mark) characters added by certain editors
5
+ 2. Non-UTF-8 encoded files (e.g., Latin-1, Windows-1252)
6
+
7
+ Issues handled:
8
+ - BOM (U+FEFF): invisible character causing "invalid non-printable character U+FEFF"
9
+ - Non-UTF-8: files with different encodings causing UnicodeDecodeError
10
+
11
+ Solution: Detect encoding with fallback chain, strip BOM, and parse quietly.
12
+ """
13
+
14
+ import ast
15
+ import warnings
16
+ from typing import Tuple
17
+
18
+
19
+ def remove_bom(source: str) -> str:
20
+ """Remove UTF-8 BOM (Byte Order Mark) from source code if present.
21
+
22
+ BOM is a special character (U+FEFF) that some editors (especially Notepad
23
+ on Windows) add to the start of files. Python's AST parser doesn't handle it.
24
+
25
+ This function silently removes it if present, or returns the source unchanged.
26
+
27
+ Args:
28
+ source: Python source code as string
29
+
30
+ Returns:
31
+ Source code with BOM removed if present
32
+
33
+ Example:
34
+ >>> source_with_bom = '\\ufeffdef hello(): pass'
35
+ >>> clean_source = remove_bom(source_with_bom)
36
+ >>> print(clean_source)
37
+ def hello(): pass
38
+ """
39
+ if source.startswith('\ufeff'):
40
+ return source[1:]
41
+ return source
42
+
43
+
44
+ def detect_encoding(file_path: str) -> Tuple[str, bool]:
45
+ """Detect file encoding by trying multiple decodings.
46
+
47
+ Tries encodings in this order:
48
+ 1. UTF-8 (most common for Python files)
49
+ 2. System default encoding
50
+ 3. Latin-1 / ISO-8859-1 (accepts any byte sequence)
51
+
52
+ Args:
53
+ file_path: Path to file to detect encoding for
54
+
55
+ Returns:
56
+ Tuple of (encoding_name: str, is_fallback: bool)
57
+ is_fallback=True means file uses non-standard encoding
58
+
59
+ Raises:
60
+ FileNotFoundError: If file doesn't exist
61
+ """
62
+ import sys
63
+
64
+ encodings_to_try = [
65
+ ('utf-8', False),
66
+ (sys.getdefaultencoding(), False),
67
+ ('latin-1', True), # Latin-1 accepts any byte sequence
68
+ ]
69
+
70
+ for encoding, is_fallback in encodings_to_try:
71
+ try:
72
+ with open(file_path, 'rb') as f:
73
+ f.read().decode(encoding)
74
+ return (encoding, is_fallback)
75
+ except (UnicodeDecodeError, LookupError):
76
+ continue
77
+
78
+ # Should never reach here since Latin-1 accepts all bytes
79
+ return ('latin-1', True)
80
+
81
+
82
+ def read_source_file(file_path: str) -> str:
83
+ """Read a Python file with automatic encoding detection and BOM removal.
84
+
85
+ Handles files with different encodings gracefully by trying multiple
86
+ decodings in order of likelihood, then falling back to Latin-1.
87
+
88
+ Args:
89
+ file_path: Path to Python file to read
90
+
91
+ Returns:
92
+ Source code with BOM removed
93
+
94
+ Raises:
95
+ FileNotFoundError: If file doesn't exist
96
+ """
97
+ encoding, _is_fallback = detect_encoding(file_path)
98
+ with open(file_path, 'r', encoding=encoding, errors='replace') as f:
99
+ source = f.read()
100
+ return remove_bom(source)
101
+
102
+
103
+ def parse_source_to_ast(source: str, file_path: str = "<unknown>") -> ast.AST:
104
+ """Parse source code while suppressing noisy invalid-escape warnings.
105
+
106
+ Some user repositories contain regular string literals like ``"\\S"`` or
107
+ ``"\\["``. Python can emit ``SyntaxWarning: invalid escape sequence`` while
108
+ parsing those files even though analysis can continue normally. For CodeMap,
109
+ these warnings are implementation noise, so we suppress them here.
110
+ """
111
+ with warnings.catch_warnings():
112
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
113
+ return ast.parse(source, filename=file_path)
114
+
115
+
116
+ def read_and_parse_python_file(file_path: str) -> ast.AST:
117
+ """Read a Python file with encoding/BOM handling and return its AST."""
118
+ source = read_source_file(file_path)
119
+ return parse_source_to_ast(source, file_path=file_path)
@@ -0,0 +1,85 @@
1
+ """Simple progress spinner for terminal output."""
2
+
3
+ import sys
4
+ import time
5
+ from threading import Thread
6
+ from typing import Optional
7
+
8
+
9
+ class ProgressSpinner:
10
+ """A simple terminal progress spinner with rotating animation.
11
+
12
+ Shows a rotating spinner animation while a long-running operation is in progress.
13
+ Useful for giving users feedback that the program is still working.
14
+
15
+ Example:
16
+ spinner = ProgressSpinner("Analyzing repository...")
17
+ spinner.start()
18
+ time.sleep(5) # Long operation
19
+ spinner.stop()
20
+ """
21
+
22
+ FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
23
+ # Fallback for terminals that don't support Unicode
24
+ FRAMES_ASCII = ['|', '/', '-', '\\']
25
+
26
+ def __init__(self, message: str = "Processing..."):
27
+ """Initialize the spinner.
28
+
29
+ Args:
30
+ message: Text to display next to the spinner
31
+ """
32
+ self.message = message
33
+ self.running = False
34
+ self.thread: Optional[Thread] = None
35
+ self.frame_index = 0
36
+ self._use_unicode = True
37
+
38
+ def _animate(self) -> None:
39
+ """Animation loop for the spinner."""
40
+ frames = self.FRAMES if self._use_unicode else self.FRAMES_ASCII
41
+
42
+ while self.running:
43
+ frame = frames[self.frame_index % len(frames)]
44
+ # Use \r to return to start of line, overwrite previous output
45
+ sys.stderr.write(f"\r{frame} {self.message}")
46
+ sys.stderr.flush()
47
+
48
+ self.frame_index += 1
49
+ time.sleep(0.1) # Smooth animation at ~10 FPS
50
+
51
+ def start(self) -> None:
52
+ """Start the spinner animation."""
53
+ if not self.running:
54
+ self.running = True
55
+ self.frame_index = 0
56
+ self.thread = Thread(target=self._animate, daemon=True)
57
+ self.thread.start()
58
+
59
+ def stop(self) -> None:
60
+ """Stop the spinner and clear the line."""
61
+ if self.running:
62
+ self.running = False
63
+ if self.thread:
64
+ self.thread.join(timeout=1)
65
+ # Clear the spinner line
66
+ sys.stderr.write("\r" + " " * (len(self.message) + 4) + "\r")
67
+ sys.stderr.flush()
68
+
69
+ def update(self, message: str) -> None:
70
+ """Update the message displayed with the spinner.
71
+
72
+ Args:
73
+ message: New message text
74
+ """
75
+ self.message = message
76
+
77
+ def __enter__(self):
78
+ """Context manager entry."""
79
+ self.start()
80
+ return self
81
+
82
+ def __exit__(self, exc_type, exc_val, exc_tb):
83
+ """Context manager exit."""
84
+ self.stop()
85
+ return False
@@ -1112,6 +1112,7 @@ def api_analyze(args) -> int:
1112
1112
  from analysis.architecture.dependency_cycles import compute_dependency_cycle_metrics
1113
1113
  from analysis.architecture.risk_radar import compute_risk_radar
1114
1114
  from analysis.utils.repo_fetcher import fetch_public_repo, fetch_public_repo_zip
1115
+ from analysis.utils.progress_spinner import ProgressSpinner
1115
1116
  from analysis.utils.cache_manager import (
1116
1117
  build_manifest,
1117
1118
  collect_fingerprints,
@@ -1282,6 +1283,10 @@ def api_analyze(args) -> int:
1282
1283
  r2 = {}
1283
1284
  metrics = {}
1284
1285
 
1286
+ # Start progress spinner for large repositories
1287
+ spinner = ProgressSpinner("Analyzing repository...")
1288
+ spinner.start()
1289
+
1285
1290
  try:
1286
1291
  if rebuild_required:
1287
1292
  r1 = run_phase4(
@@ -1399,9 +1404,11 @@ def api_analyze(args) -> int:
1399
1404
  )
1400
1405
  touch_last_accessed(repo_hash)
1401
1406
  except Exception as e:
1407
+ spinner.stop()
1402
1408
  print_json({"ok": False, "error": "ANALYZE_FAILED", "message": redact_secrets(str(e))})
1403
1409
  return 1
1404
1410
 
1411
+ spinner.stop()
1405
1412
  print_json({
1406
1413
  "ok": True,
1407
1414
  "source": source,
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codemap-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Local Python code analysis tool - understand architecture, dependencies, and call graphs
5
5
  Author-email: ADITYA <aditykushwaha69@gmail.com>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/ADITYA-kus/codemap_ai
8
8
  Project-URL: Repository, https://github.com/ADITYA-kus/codemap_ai.git
9
9
  Project-URL: Issues, https://github.com/ADITYA-kus/codemap_ai/issues
@@ -11,7 +11,6 @@ Project-URL: Documentation, https://github.com/ADITYA-kus/codemap_ai#readme
11
11
  Keywords: code-analysis,python,architecture,call-graph,cli,dashboard,local,privacy
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
@@ -159,22 +158,35 @@ codemap open --port 8000
159
158
 
160
159
  ### Cache Management
161
160
  ```bash
162
- # List all analyzed repositories
161
+ # 📋 List all analyzed repositories and their cache info
163
162
  codemap cache list
164
163
 
165
- # Show cache details for a repository
166
- codemap cache info <repo_hash>
164
+ # 📊 Show detailed cache information for a specific repository
165
+ codemap cache info --path <repo_directory>
167
166
 
168
- # Clear a specific repository's cache
169
- codemap cache clear <repo_hash>
167
+ # ⏱️ Set cache retention policy (automatically clean old caches)
168
+ codemap cache retention --path <repo_directory> --days 30 --yes
170
169
 
171
- # Show cache retention policy
172
- codemap cache retention <repo_hash>
170
+ # 🧹 Preview what would be cleaned (safe, no deletion)
171
+ codemap cache sweep --dry-run
173
172
 
174
- # Sweep expired caches (auto-cleanup)
175
- codemap cache sweep
173
+ # 🧹 Actually clean up expired caches (requires --yes confirmation)
174
+ codemap cache sweep --yes
175
+
176
+ # 🗑️ Clear cache for a specific repository (preview first)
177
+ codemap cache clear --path <repo_directory> --dry-run
178
+
179
+ # 🗑️ Actually delete a repository's cache (requires --yes confirmation)
180
+ codemap cache clear --path <repo_directory> --yes
176
181
  ```
177
182
 
183
+ **Cache Management Tips:**
184
+ - ✅ Always use `--dry-run` first to preview changes
185
+ - ✅ Add `--yes` flag to skip confirmation (useful in scripts)
186
+ - ✅ Default retention is 14 days; adjust with `--days <number>`
187
+ - ✅ Cache is stored in: `~/.codemap_cache/` (varies by OS)
188
+ - ✅ Use `cache list` to see all cached repositories and their sizes
189
+
178
190
  **Get GitHub Token (for private repos):**
179
191
  1. Go to https://github.com/settings/tokens
180
192
  2. Click "Generate new token" → "Generate new token (classic)"
@@ -42,8 +42,10 @@ analysis/runners/__init__.py
42
42
  analysis/runners/phase4_runner.py
43
43
  analysis/utils/__init__.py
44
44
  analysis/utils/ast_helpers.py
45
+ analysis/utils/bom_handler.py
45
46
  analysis/utils/cache_manager.py
46
47
  analysis/utils/path_resolver.py
48
+ analysis/utils/progress_spinner.py
47
49
  analysis/utils/repo_fetcher.py
48
50
  codemap_python.egg-info/PKG-INFO
49
51
  codemap_python.egg-info/SOURCES.txt
@@ -53,6 +55,7 @@ codemap_python.egg-info/requires.txt
53
55
  codemap_python.egg-info/top_level.txt
54
56
  tests/test_cache_cli_commands.py
55
57
  tests/test_cache_retention.py
58
+ tests/test_cli_invalid_escape_warnings.py
56
59
  tests/test_no_key_persistence.py
57
60
  tests/test_registry_session_mode.py
58
61
  tests/test_security_cli_integration.py
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codemap-python"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Local Python code analysis tool - understand architecture, dependencies, and call graphs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
- license = {text = "MIT"}
11
+ license = "MIT"
12
12
  authors = [
13
13
  {name = "ADITYA", email = "aditykushwaha69@gmail.com"}
14
14
  ]
@@ -25,7 +25,6 @@ keywords = [
25
25
  classifiers = [
26
26
  "Development Status :: 4 - Beta",
27
27
  "Intended Audience :: Developers",
28
- "License :: OSI Approved :: MIT License",
29
28
  "Programming Language :: Python :: 3",
30
29
  "Programming Language :: Python :: 3.10",
31
30
  "Programming Language :: Python :: 3.11",
@@ -0,0 +1,35 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ import unittest
6
+
7
+
8
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+
11
+ class TestCliInvalidEscapeWarnings(unittest.TestCase):
12
+ def test_analyze_suppresses_invalid_escape_syntax_warnings(self):
13
+ repo_dir = os.path.join(PROJECT_ROOT, "tests", "_tmp_invalid_escape_repo")
14
+ shutil.rmtree(repo_dir, ignore_errors=True)
15
+ try:
16
+ os.makedirs(repo_dir, exist_ok=True)
17
+ with open(os.path.join(repo_dir, "warns.py"), "w", encoding="utf-8") as f:
18
+ f.write(
19
+ 'PATTERNS = ["\\\\S", "\\\\[", "\\\\:", "\\\\d"]\n'
20
+ "def read_patterns():\n"
21
+ " return PATTERNS\n"
22
+ )
23
+
24
+ cmd = [sys.executable, os.path.join(PROJECT_ROOT, "cli.py"), "analyze", "--path", repo_dir]
25
+ proc = subprocess.run(cmd, cwd=PROJECT_ROOT, capture_output=True, text=True, check=False)
26
+
27
+ self.assertEqual(proc.returncode, 0, msg=proc.stderr or proc.stdout)
28
+ self.assertNotIn("SyntaxWarning", proc.stderr)
29
+ self.assertNotIn("invalid escape sequence", proc.stderr)
30
+ finally:
31
+ shutil.rmtree(repo_dir, ignore_errors=True)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ unittest.main()
@@ -893,7 +893,11 @@ def _push_recent(items: List[str], value: str, limit: int = 20) -> List[str]:
893
893
 
894
894
  @app.get("/", response_class=HTMLResponse)
895
895
  def index(request: Request):
896
- return templates.TemplateResponse("index.html", {"request": request, "default_repo": DEFAULT_REPO})
896
+ return templates.TemplateResponse(
897
+ request=request,
898
+ name="index.html",
899
+ context={"request": request, "default_repo": DEFAULT_REPO}
900
+ )
897
901
 
898
902
 
899
903
  @app.get("/api/workspace")
@@ -98,6 +98,15 @@
98
98
  let graphDataCache = new Map();
99
99
  let impactDataCache = new Map();
100
100
  let architectureCache = null;
101
+
102
+ // Track which tabs have been loaded (for lazy loading optimization)
103
+ let tabsLoaded = {
104
+ details: true, // Details tab loads on symbol selection
105
+ graph: false, // Lazy load when clicked
106
+ impact: false, // Lazy load when clicked
107
+ architecture: false // Lazy load when clicked
108
+ };
109
+
101
110
  let repoSummary = null;
102
111
  let repoSummaryUpdatedAt = "";
103
112
  let repoSummaryStatus = "idle"; // idle|loading|ready|missing|error
@@ -523,26 +532,61 @@
523
532
  const isImpact = activeTab === "impact";
524
533
  const isGraph = activeTab === "graph";
525
534
  const isArchitecture = activeTab === "architecture";
535
+
536
+ // Update tab button states
526
537
  if (tabDetailsEl) tabDetailsEl.classList.toggle("active", activeTab === "details");
527
538
  if (tabImpactEl) tabImpactEl.classList.toggle("active", isImpact);
528
539
  if (tabGraphEl) tabGraphEl.classList.toggle("active", isGraph);
529
540
  if (tabArchitectureEl) tabArchitectureEl.classList.toggle("active", isArchitecture);
541
+
542
+ // Show/hide tab content
530
543
  if (symbolViewEl) symbolViewEl.classList.toggle("hidden", activeTab !== "details");
531
544
  if (impactViewEl) impactViewEl.classList.toggle("hidden", !isImpact);
532
545
  if (graphViewEl) graphViewEl.classList.toggle("hidden", !isGraph);
533
546
  if (architectureViewEl) architectureViewEl.classList.toggle("hidden", !isArchitecture);
534
547
  if (graphControlsEl) graphControlsEl.classList.toggle("hidden", !isGraph);
535
548
  if (impactControlsEl) impactControlsEl.classList.toggle("hidden", !isImpact);
536
- if (isGraph) {
549
+
550
+ // Lazy loading: Only load data if not already loaded
551
+ // This prevents repeated API calls and speed up tab switching
552
+ if (isGraph && !tabsLoaded.graph) {
553
+ showLoadingIndicator(graphViewEl, "Loading call graph...");
537
554
  loadGraph();
538
- }
539
- if (isImpact) {
555
+ tabsLoaded.graph = true;
556
+ } else if (isGraph && tabsLoaded.graph) {
557
+ // Already loaded, just make sure content is visible
558
+ if (graphViewEl) graphViewEl.classList.remove("hidden");
559
+ }
560
+
561
+ if (isImpact && !tabsLoaded.impact) {
562
+ showLoadingIndicator(impactViewEl, "Loading impact analysis...");
540
563
  loadImpact();
564
+ tabsLoaded.impact = true;
565
+ } else if (isImpact && tabsLoaded.impact) {
566
+ if (impactViewEl) impactViewEl.classList.remove("hidden");
541
567
  }
542
- if (isArchitecture) {
568
+
569
+ if (isArchitecture && !tabsLoaded.architecture) {
570
+ showLoadingIndicator(architectureViewEl, "Loading architecture insights...");
543
571
  loadArchitecture();
572
+ tabsLoaded.architecture = true;
573
+ } else if (isArchitecture && tabsLoaded.architecture) {
574
+ if (architectureViewEl) architectureViewEl.classList.remove("hidden");
544
575
  }
545
576
  }
577
+
578
+ // Show loading indicator in a view element
579
+ function showLoadingIndicator(el, message) {
580
+ if (!el) return;
581
+ el.classList.remove("muted", "hidden");
582
+ el.innerHTML = `<div class="card" style="padding: 20px; text-align: center;">
583
+ <div style="display: inline-block; width: 24px; height: 24px; border: 3px solid #ccc; border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 10px;"></div>
584
+ <span>${esc(message)}</span>
585
+ </div>
586
+ <style>
587
+ @keyframes spin { to { transform: rotate(360deg); } }
588
+ </style>`;
589
+ }
546
590
 
547
591
  function shortLabel(fqn) {
548
592
  const parts = String(fqn || "").split(".");
@@ -2585,6 +2629,12 @@
2585
2629
  highlightActiveSymbol();
2586
2630
  graphDataCache.clear();
2587
2631
  impactDataCache.clear();
2632
+
2633
+ // Allow graph and impact tabs to be reloaded for the new symbol
2634
+ // (Keep details: true since we're loading it now)
2635
+ tabsLoaded.graph = false;
2636
+ tabsLoaded.impact = false;
2637
+
2588
2638
  showSymbolLoading();
2589
2639
  try {
2590
2640
  const symbolPromise = (async () => {
@@ -2716,6 +2766,15 @@
2716
2766
  riskRadarUpdatedAt = "";
2717
2767
  riskRadarStatus = "idle";
2718
2768
  riskRadarError = "";
2769
+
2770
+ // Reset tab loading state for new repo (enables lazy loading again)
2771
+ tabsLoaded = {
2772
+ details: true,
2773
+ graph: false,
2774
+ impact: false,
2775
+ architecture: false
2776
+ };
2777
+
2719
2778
  await loadRepoRegistry();
2720
2779
  const okMeta = await loadMeta();
2721
2780
  await loadDataPrivacy();
@@ -1,8 +0,0 @@
1
- # AST Parser Module
2
- import ast
3
- def parse_python_file(file_path):
4
- with open(file_path, "r", encoding="utf-8") as f:
5
- source = f.read()
6
-
7
- return ast.parse(source)
8
-
File without changes