pycodetags 0.1.0__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.
- code_tags/__about__.py +8 -0
- code_tags/__init__.py +38 -0
- code_tags/__main__.py +233 -0
- code_tags/aggregate.py +130 -0
- code_tags/collect.py +271 -0
- code_tags/collect_ast.py +80 -0
- code_tags/collection_types.py +15 -0
- code_tags/comment_finder.py +181 -0
- code_tags/config.py +363 -0
- code_tags/converters.py +111 -0
- code_tags/data_tag_types.py +163 -0
- code_tags/data_tags.py +397 -0
- code_tags/dotenv.py +101 -0
- code_tags/folk_code_tags.py +256 -0
- code_tags/logging_config.py +69 -0
- code_tags/plugin_diagnostics.py +30 -0
- code_tags/plugin_manager.py +21 -0
- code_tags/plugin_specs.py +120 -0
- code_tags/py.typed +1 -0
- code_tags/specific_schemas.py +26 -0
- code_tags/standard_code_tags.py +171 -0
- code_tags/todo_object_schema.py +30 -0
- code_tags/todo_tag_types.py +364 -0
- code_tags/todo_tag_types_aliases.py +535 -0
- code_tags/todo_tag_types_generate_aliases.py +111 -0
- code_tags/user.py +48 -0
- code_tags/users_from_authors.py +95 -0
- code_tags/view_tools.py +45 -0
- code_tags/views.py +289 -0
- pycodetags-0.1.0.dist-info/METADATA +28 -0
- pycodetags-0.1.0.dist-info/RECORD +35 -0
- pycodetags-0.1.0.dist-info/WHEEL +4 -0
- pycodetags-0.1.0.dist-info/entry_points.txt +4 -0
- pycodetags-0.1.0.dist-info/licenses/AUTHORS.md +1 -0
- pycodetags-0.1.0.dist-info/licenses/LICENSE +21 -0
code_tags/__about__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Metadata for code_tags."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["__title__", "__version__", "__description__", "__requires_python__"]
|
|
4
|
+
|
|
5
|
+
__title__ = "code_tags"
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__description__ = "TODOs in source code as a first class construct"
|
|
8
|
+
__requires_python__ = ">=3.9"
|
code_tags/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Tags is a tool and library for putting TODOs into source code.
|
|
3
|
+
|
|
4
|
+
Only the strongly typed decorators, exceptions and context managers are exported.
|
|
5
|
+
|
|
6
|
+
Everything else is a tool.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"TODO",
|
|
11
|
+
"FIXME",
|
|
12
|
+
"TodoException",
|
|
13
|
+
"REQUIREMENT",
|
|
14
|
+
"STORY",
|
|
15
|
+
"IDEA",
|
|
16
|
+
"BUG",
|
|
17
|
+
"HACK",
|
|
18
|
+
"CLEVER",
|
|
19
|
+
"MAGIC",
|
|
20
|
+
"ALERT",
|
|
21
|
+
"PORT",
|
|
22
|
+
"DOCUMENT",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
from code_tags.todo_tag_types import TODO, TodoException
|
|
26
|
+
from code_tags.todo_tag_types_aliases import (
|
|
27
|
+
ALERT,
|
|
28
|
+
BUG,
|
|
29
|
+
CLEVER,
|
|
30
|
+
DOCUMENT,
|
|
31
|
+
FIXME,
|
|
32
|
+
HACK,
|
|
33
|
+
IDEA,
|
|
34
|
+
MAGIC,
|
|
35
|
+
PORT,
|
|
36
|
+
REQUIREMENT,
|
|
37
|
+
STORY,
|
|
38
|
+
)
|
code_tags/__main__.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for code_tags.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import logging.config
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
import pluggy
|
|
14
|
+
|
|
15
|
+
import code_tags.__about__ as __about__
|
|
16
|
+
from code_tags.aggregate import aggregate_all_kinds, aggregate_all_kinds_multiple_input, merge_collected
|
|
17
|
+
from code_tags.collection_types import CollectedTODOs
|
|
18
|
+
from code_tags.config import CodeTagsConfig, get_code_tags_config
|
|
19
|
+
from code_tags.dotenv import load_dotenv
|
|
20
|
+
from code_tags.logging_config import generate_config
|
|
21
|
+
from code_tags.plugin_diagnostics import plugin_currently_loaded
|
|
22
|
+
from code_tags.plugin_manager import get_plugin_manager
|
|
23
|
+
from code_tags.views import (
|
|
24
|
+
print_changelog,
|
|
25
|
+
print_done_file,
|
|
26
|
+
print_html,
|
|
27
|
+
print_json,
|
|
28
|
+
print_text,
|
|
29
|
+
print_todo_md,
|
|
30
|
+
print_validate,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Main entry point for the code_tags CLI.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
argv (Sequence[str] | None): Command line arguments. If None, uses sys.argv.
|
|
40
|
+
"""
|
|
41
|
+
pm = get_plugin_manager()
|
|
42
|
+
|
|
43
|
+
class InternalViews:
|
|
44
|
+
"""Register internal views as a plugin"""
|
|
45
|
+
|
|
46
|
+
@pluggy.HookimplMarker("code_tags")
|
|
47
|
+
def code_tags_print_report(self, format_name: str, found_data: CollectedTODOs) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Internal method to handle printing of reports in various formats.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
format_name (str): The name of the format requested by the user.
|
|
53
|
+
found_data (CollectedTODOs): The data collected from the source code.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
bool: True if the format was handled, False otherwise.
|
|
57
|
+
"""
|
|
58
|
+
if format_name == "text":
|
|
59
|
+
print_text(found_data)
|
|
60
|
+
return True
|
|
61
|
+
if format_name == "html":
|
|
62
|
+
print_html(found_data)
|
|
63
|
+
return True
|
|
64
|
+
if format_name == "json":
|
|
65
|
+
print_json(found_data)
|
|
66
|
+
return True
|
|
67
|
+
if format_name == "keep-a-changelog":
|
|
68
|
+
print_changelog(found_data)
|
|
69
|
+
return True
|
|
70
|
+
if format_name == "todo.md":
|
|
71
|
+
print_todo_md(found_data)
|
|
72
|
+
return True
|
|
73
|
+
if format_name == "done":
|
|
74
|
+
print_done_file(found_data)
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
pm.register(InternalViews())
|
|
79
|
+
# --- end pluggy setup ---
|
|
80
|
+
|
|
81
|
+
parser = argparse.ArgumentParser(description=f"{__about__.__description__} (v{__about__.__version__})")
|
|
82
|
+
|
|
83
|
+
# Basic arguments that apply to all commands (like verbose/info/bug-trail/config)
|
|
84
|
+
base_parser = argparse.ArgumentParser(add_help=False)
|
|
85
|
+
base_parser.add_argument("--config", help="Path to config file, defaults to current folder pyproject.toml")
|
|
86
|
+
base_parser.add_argument("--verbose", default=False, action="store_true", help="verbose level logging output")
|
|
87
|
+
base_parser.add_argument("--info", default=False, action="store_true", help="info level logging output")
|
|
88
|
+
base_parser.add_argument("--bug-trail", default=False, action="store_true", help="enable bug trail, local logging")
|
|
89
|
+
# validate switch
|
|
90
|
+
base_parser.add_argument("--validate", action="store_true", help="Validate all the items found")
|
|
91
|
+
|
|
92
|
+
# Create subparsers for commands
|
|
93
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
94
|
+
|
|
95
|
+
# 'report' command
|
|
96
|
+
report_parser = subparsers.add_parser("report", parents=[base_parser], help="Generate code tag reports")
|
|
97
|
+
# report runs collectors, collected things can be validated
|
|
98
|
+
report_parser.add_argument("--module", action="append", help="Python module to inspect (e.g., 'my_project.main')")
|
|
99
|
+
report_parser.add_argument("--src", action="append", help="file or folder of source code")
|
|
100
|
+
|
|
101
|
+
report_parser.add_argument("--output", help="destination file or folder")
|
|
102
|
+
|
|
103
|
+
extra_supported_formats = []
|
|
104
|
+
for result in pm.hook.code_tags_print_report_style_name():
|
|
105
|
+
extra_supported_formats.extend(result)
|
|
106
|
+
|
|
107
|
+
report_parser.add_argument(
|
|
108
|
+
"--format",
|
|
109
|
+
choices=["text", "html", "json", "keep-a-changelog", "todo.md", "done"] + extra_supported_formats,
|
|
110
|
+
default="text",
|
|
111
|
+
help="Output format for the report.",
|
|
112
|
+
)
|
|
113
|
+
# report_parser.add_argument("--validate", action="store_true", help="Validate all the items found")
|
|
114
|
+
|
|
115
|
+
_plugin_info_parser = subparsers.add_parser(
|
|
116
|
+
"plugin-info", parents=[base_parser], help="Display information about loaded plugins"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Allow plugins to add their own subparsers
|
|
120
|
+
new_subparsers = pm.hook.code_tags_add_cli_subcommands(subparsers=subparsers)
|
|
121
|
+
# Hack because we don't want plugins to have to wire up the basic stuff
|
|
122
|
+
for new_subparser in new_subparsers:
|
|
123
|
+
new_subparser.add_argument("--config", help="Path to config file, defaults to current folder pyproject.toml")
|
|
124
|
+
new_subparser.add_argument("--verbose", default=False, action="store_true", help="verbose level logging output")
|
|
125
|
+
new_subparser.add_argument("--info", default=False, action="store_true", help="info level logging output")
|
|
126
|
+
new_subparser.add_argument(
|
|
127
|
+
"--bug-trail", default=False, action="store_true", help="enable bug trail, local logging"
|
|
128
|
+
)
|
|
129
|
+
# validate switch
|
|
130
|
+
new_subparser.add_argument("--validate", action="store_true", help="Validate all the items found")
|
|
131
|
+
|
|
132
|
+
args = parser.parse_args(args=argv)
|
|
133
|
+
|
|
134
|
+
if args.config:
|
|
135
|
+
code_tags_config = CodeTagsConfig(pyproject_path=args.config)
|
|
136
|
+
else:
|
|
137
|
+
code_tags_config = CodeTagsConfig()
|
|
138
|
+
|
|
139
|
+
if code_tags_config.use_dot_env():
|
|
140
|
+
load_dotenv()
|
|
141
|
+
|
|
142
|
+
if args.verbose:
|
|
143
|
+
config = generate_config(level="DEBUG", enable_bug_trail=args.bug_trail)
|
|
144
|
+
logging.config.dictConfig(config)
|
|
145
|
+
elif args.info:
|
|
146
|
+
config = generate_config(level="INFO", enable_bug_trail=args.bug_trail)
|
|
147
|
+
logging.config.dictConfig(config)
|
|
148
|
+
else:
|
|
149
|
+
# Essentially, quiet mode
|
|
150
|
+
config = generate_config(level="FATAL", enable_bug_trail=args.bug_trail)
|
|
151
|
+
logging.config.dictConfig(config)
|
|
152
|
+
|
|
153
|
+
if not args.command:
|
|
154
|
+
parser.print_help()
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
# Handle the 'report' command
|
|
158
|
+
if args.command == "report":
|
|
159
|
+
modules = args.module or code_tags_config.modules_to_scan()
|
|
160
|
+
src = args.src or code_tags_config.source_folders_to_scan()
|
|
161
|
+
if not modules and not src:
|
|
162
|
+
print(
|
|
163
|
+
"Need to specify one or more importable modules (--module) "
|
|
164
|
+
"or source code folders/files (--src) or specify in config file.",
|
|
165
|
+
file=sys.stderr,
|
|
166
|
+
)
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
found = aggregate_all_kinds_multiple_input(modules, src)
|
|
171
|
+
except ImportError:
|
|
172
|
+
print(f"Error: Could not import module(s) '{args.module}'", file=sys.stderr)
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
if args.validate:
|
|
176
|
+
if len(found["todos"]) + len(found["exceptions"]) == 0:
|
|
177
|
+
raise TypeError("No data to validate.")
|
|
178
|
+
print_validate(found)
|
|
179
|
+
else:
|
|
180
|
+
if len(found["todos"]) + len(found["exceptions"]) == 0:
|
|
181
|
+
raise TypeError("No data to report.")
|
|
182
|
+
# Call the hook.
|
|
183
|
+
results = pm.hook.code_tags_print_report(
|
|
184
|
+
format_name=args.format, output_path=args.output, found_data=found, config=get_code_tags_config()
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# results = pm.hook.code_tags_print_report(format_name=args.format, found_data=found)
|
|
188
|
+
if not any(results):
|
|
189
|
+
print(f"Error: Format '{args.format}' is not supported.", file=sys.stderr)
|
|
190
|
+
return 1
|
|
191
|
+
# --- NEW: Handle 'plugin-info' command ---
|
|
192
|
+
elif args.command == "plugin-info":
|
|
193
|
+
plugin_currently_loaded(pm)
|
|
194
|
+
else:
|
|
195
|
+
# Pass control to plugins for other commands
|
|
196
|
+
# Aggregate data if plugins might need it
|
|
197
|
+
found_data_for_plugins: CollectedTODOs = {}
|
|
198
|
+
modules = []
|
|
199
|
+
src = []
|
|
200
|
+
if hasattr(args, "module") and args.module:
|
|
201
|
+
modules = getattr(args, "module", [])
|
|
202
|
+
else:
|
|
203
|
+
modules = code_tags_config.modules_to_scan()
|
|
204
|
+
|
|
205
|
+
if hasattr(args, "src") and args.src:
|
|
206
|
+
src = getattr(args, "src", [])
|
|
207
|
+
else:
|
|
208
|
+
modules = code_tags_config.source_folders_to_scan()
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# BUG: this needs to be a list
|
|
212
|
+
all_found: list[CollectedTODOs] = []
|
|
213
|
+
for source in src:
|
|
214
|
+
all_found.append(aggregate_all_kinds("", source))
|
|
215
|
+
for module in modules:
|
|
216
|
+
all_found.append(aggregate_all_kinds(module, ""))
|
|
217
|
+
|
|
218
|
+
found_data_for_plugins = merge_collected(all_found)
|
|
219
|
+
except ImportError:
|
|
220
|
+
logging.warning(f"Could not aggregate data for command {args.command}, proceeding without it.")
|
|
221
|
+
found_data_for_plugins = {"todos": [], "exceptions": []}
|
|
222
|
+
|
|
223
|
+
handled_by_plugin = pm.hook.code_tags_run_cli_command(
|
|
224
|
+
command_name=args.command, args=args, found_data=found_data_for_plugins, config=get_code_tags_config()
|
|
225
|
+
)
|
|
226
|
+
if not any(handled_by_plugin):
|
|
227
|
+
print(f"Error: Unknown command '{args.command}'.", file=sys.stderr)
|
|
228
|
+
return 1
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
sys.exit(main())
|
code_tags/aggregate.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aggregate live module and source files for all known schemas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import logging
|
|
9
|
+
import logging.config
|
|
10
|
+
import pathlib
|
|
11
|
+
|
|
12
|
+
import code_tags.folk_code_tags as folk_code_tags
|
|
13
|
+
import code_tags.standard_code_tags as standard_code_tags
|
|
14
|
+
from code_tags.collect import collect_all_todos
|
|
15
|
+
from code_tags.collection_types import CollectedTODOs
|
|
16
|
+
from code_tags.config import get_code_tags_config
|
|
17
|
+
from code_tags.converters import convert_folk_tag_to_TODO, convert_pep350_tag_to_TODO
|
|
18
|
+
from code_tags.plugin_manager import get_plugin_manager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def merge_collected(all_found: list[CollectedTODOs]) -> CollectedTODOs:
|
|
24
|
+
merged: CollectedTODOs = {"todos": [], "exceptions": []}
|
|
25
|
+
for found in all_found:
|
|
26
|
+
merged["todos"] += found.get("todos", [])
|
|
27
|
+
merged["exceptions"] += found.get("exceptions", [])
|
|
28
|
+
return merged
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def aggregate_all_kinds_multiple_input(module_names: list[str], source_paths: list[str]) -> CollectedTODOs:
|
|
32
|
+
"""Refactor to support lists of modules and lists of source paths"""
|
|
33
|
+
if not module_names:
|
|
34
|
+
module_names = []
|
|
35
|
+
if not source_paths:
|
|
36
|
+
source_paths = []
|
|
37
|
+
collected: CollectedTODOs = {"todos": [], "exceptions": []}
|
|
38
|
+
|
|
39
|
+
for module_name in module_names:
|
|
40
|
+
found = aggregate_all_kinds(module_name, "")
|
|
41
|
+
collected["todos"] += found["todos"]
|
|
42
|
+
collected["exceptions"] += found["exceptions"]
|
|
43
|
+
for source_path in source_paths:
|
|
44
|
+
found = aggregate_all_kinds("", source_path)
|
|
45
|
+
collected["todos"] += found["todos"]
|
|
46
|
+
collected["exceptions"] += found["exceptions"]
|
|
47
|
+
return collected
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def aggregate_all_kinds(module_name: str, source_path: str) -> CollectedTODOs:
|
|
51
|
+
"""
|
|
52
|
+
Aggregate all TODOs and DONEs from a module and source files.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
module_name (str): The name of the module to search in.
|
|
56
|
+
source_path (str): The path to the source files.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
CollectedTODOs: A dictionary containing collected TODOs, DONEs, and exceptions.
|
|
60
|
+
"""
|
|
61
|
+
config = get_code_tags_config()
|
|
62
|
+
|
|
63
|
+
active_schemas = config.active_schemas()
|
|
64
|
+
all_schemas = False
|
|
65
|
+
if not active_schemas:
|
|
66
|
+
all_schemas = True
|
|
67
|
+
|
|
68
|
+
pm = get_plugin_manager()
|
|
69
|
+
found: CollectedTODOs = {}
|
|
70
|
+
if bool(module_name):
|
|
71
|
+
logging.info(f"Checking {module_name}")
|
|
72
|
+
module = importlib.import_module(module_name)
|
|
73
|
+
|
|
74
|
+
found = collect_all_todos(module, include_submodules=False, include_exceptions=True)
|
|
75
|
+
|
|
76
|
+
found_folk_code_tags = []
|
|
77
|
+
found_pep350_code_tags = []
|
|
78
|
+
|
|
79
|
+
if source_path:
|
|
80
|
+
src_found = 0
|
|
81
|
+
path = pathlib.Path(source_path)
|
|
82
|
+
files = [path] if path.is_file() else path.rglob("*.*")
|
|
83
|
+
for file in files:
|
|
84
|
+
if file.name.endswith(".py"):
|
|
85
|
+
if all_schemas or "todo" in config.active_schemas():
|
|
86
|
+
found_pep350_code_tags.extend(
|
|
87
|
+
list(
|
|
88
|
+
convert_pep350_tag_to_TODO(_)
|
|
89
|
+
for _ in standard_code_tags.collect_pep350_code_tags(file=str(file))
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
src_found += 1
|
|
93
|
+
|
|
94
|
+
if all_schemas or "folk" in config.active_schemas():
|
|
95
|
+
found_folk_code_tags.extend(
|
|
96
|
+
list(convert_folk_tag_to_TODO(_) for _ in folk_code_tags.find_source_tags(str(file)))
|
|
97
|
+
)
|
|
98
|
+
src_found += 1
|
|
99
|
+
else:
|
|
100
|
+
# Collect folk tags from plugins
|
|
101
|
+
plugin_results = pm.hook.find_source_tags(
|
|
102
|
+
already_processed=False, file_path=str(file), config=get_code_tags_config()
|
|
103
|
+
)
|
|
104
|
+
for result_list in plugin_results:
|
|
105
|
+
found_folk_code_tags.extend(convert_folk_tag_to_TODO(tag) for tag in result_list)
|
|
106
|
+
if plugin_results:
|
|
107
|
+
src_found += 1
|
|
108
|
+
if src_found == 0:
|
|
109
|
+
raise TypeError(f"Can't find any files in source folder {source_path}")
|
|
110
|
+
|
|
111
|
+
folk_separated: CollectedTODOs = {"todos": found_folk_code_tags, "exceptions": []}
|
|
112
|
+
pep30_separated = {"todos": found_pep350_code_tags, "exceptions": []}
|
|
113
|
+
|
|
114
|
+
temp: CollectedTODOs = {}
|
|
115
|
+
for thing in (folk_separated, pep30_separated, found):
|
|
116
|
+
for key, value in thing.items():
|
|
117
|
+
if key not in temp:
|
|
118
|
+
# HACK: This is ugly.
|
|
119
|
+
if key == "todos":
|
|
120
|
+
temp["todos"] = value # type: ignore[typeddict-item]
|
|
121
|
+
else:
|
|
122
|
+
temp["exceptions"] = value # type: ignore[typeddict-item]
|
|
123
|
+
else:
|
|
124
|
+
if key == "todos":
|
|
125
|
+
temp["todos"].extend(value) # type: ignore[arg-type]
|
|
126
|
+
else:
|
|
127
|
+
temp["exceptions"].extend(value) # type: ignore[arg-type]
|
|
128
|
+
|
|
129
|
+
found = temp
|
|
130
|
+
return found
|
code_tags/collect.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finds all strongly typed code tags in a module.
|
|
3
|
+
|
|
4
|
+
Three ways to find strongly typed TODOs:
|
|
5
|
+
|
|
6
|
+
- import module, walk the object graph. Easy to miss anything without a public interface
|
|
7
|
+
- See other modules for techniques using AST parsing
|
|
8
|
+
- See other modules for source parsing.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import sysconfig
|
|
18
|
+
import types
|
|
19
|
+
from types import ModuleType, SimpleNamespace
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from code_tags.collect_ast import TodoExceptionCollector
|
|
23
|
+
from code_tags.collection_types import CollectedTODOs
|
|
24
|
+
from code_tags.todo_tag_types import TODO, TodoException
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_stdlib_module(module: types.ModuleType | SimpleNamespace) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Check if a module is part of the Python standard library.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
module: The module to check
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if the module is part of the standard library, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
# Built-in module (no __file__ attribute, e.g. 'sys', 'math', etc.)
|
|
40
|
+
if not hasattr(module, "__file__"):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
stdlib_path = sysconfig.get_paths()["stdlib"]
|
|
44
|
+
the_path = getattr(module, "__file__", "")
|
|
45
|
+
if not the_path:
|
|
46
|
+
return True
|
|
47
|
+
module_path = os.path.abspath(the_path)
|
|
48
|
+
|
|
49
|
+
return module_path.startswith(os.path.abspath(stdlib_path))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TodoCollector:
|
|
53
|
+
"""Comprehensive collector for TODO, Done, and TodoException items."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.todos: list[TODO] = []
|
|
57
|
+
self.todo_exceptions: list[TodoException] = []
|
|
58
|
+
self.visited: set[int] = set()
|
|
59
|
+
|
|
60
|
+
def collect_from_module(
|
|
61
|
+
self, module: ModuleType, include_submodules: bool = True, max_depth: int = 10
|
|
62
|
+
) -> tuple[list[TODO], list[TodoException]]:
|
|
63
|
+
"""
|
|
64
|
+
Collect all TODO/Done items and TodoExceptions from a module.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
module: The module to inspect
|
|
68
|
+
include_submodules: Whether to recursively inspect submodules
|
|
69
|
+
max_depth: Maximum recursion depth for submodules
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple of (todos, dones, todo_exceptions)
|
|
73
|
+
"""
|
|
74
|
+
self._reset()
|
|
75
|
+
self._collect_recursive(module, include_submodules, max_depth, 0)
|
|
76
|
+
return self.todos.copy(), self.todo_exceptions.copy()
|
|
77
|
+
|
|
78
|
+
def _reset(self) -> None:
|
|
79
|
+
"""Reset internal collections."""
|
|
80
|
+
self.todos.clear()
|
|
81
|
+
self.todo_exceptions.clear()
|
|
82
|
+
self.visited.clear()
|
|
83
|
+
|
|
84
|
+
def _collect_recursive(self, obj: Any, include_submodules: bool, max_depth: int, current_depth: int) -> None:
|
|
85
|
+
"""Recursively collect TODO/Done items from an object.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
obj: The object to inspect
|
|
89
|
+
include_submodules: Whether to recursively inspect submodules
|
|
90
|
+
max_depth: Maximum recursion depth for submodules
|
|
91
|
+
current_depth: Current recursion depth
|
|
92
|
+
"""
|
|
93
|
+
if current_depth > max_depth or id(obj) in self.visited:
|
|
94
|
+
if current_depth > max_depth:
|
|
95
|
+
logger.debug(f"Maximum depth {max_depth}")
|
|
96
|
+
else:
|
|
97
|
+
logger.debug(f"Already visited {id(obj)}")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
self.visited.add(id(obj))
|
|
101
|
+
|
|
102
|
+
# Check if object itself is a TODO/Done item
|
|
103
|
+
# self._check_object_for_todos(obj)
|
|
104
|
+
|
|
105
|
+
# Handle modules
|
|
106
|
+
if inspect.ismodule(obj) and not is_stdlib_module(obj):
|
|
107
|
+
logger.debug(f"Collecting module {obj}")
|
|
108
|
+
self._collect_from_module_attributes(obj, include_submodules, max_depth, current_depth)
|
|
109
|
+
|
|
110
|
+
if isinstance(obj, SimpleNamespace):
|
|
111
|
+
logger.debug(f"Collecting namespace {obj}")
|
|
112
|
+
self._collect_from_module_attributes(obj, include_submodules, max_depth, current_depth)
|
|
113
|
+
|
|
114
|
+
# Handle classes
|
|
115
|
+
if inspect.isclass(obj):
|
|
116
|
+
logger.debug(f"Collecting class {obj}")
|
|
117
|
+
self._collect_from_class_attributes(obj, include_submodules, max_depth, current_depth)
|
|
118
|
+
|
|
119
|
+
# Handle functions and methods
|
|
120
|
+
if inspect.isfunction(obj) or inspect.ismethod(obj):
|
|
121
|
+
logger.debug(f"Collecting function/method {obj}")
|
|
122
|
+
self._check_object_for_todos(obj)
|
|
123
|
+
# Classes are showing up as functions?! Yes.
|
|
124
|
+
self._collect_from_class_attributes(obj, include_submodules, max_depth, current_depth)
|
|
125
|
+
if isinstance(obj, (list, set, tuple)) and obj:
|
|
126
|
+
logger.debug(f"Found a list/set/tuple {obj}")
|
|
127
|
+
for item in obj:
|
|
128
|
+
self._check_object_for_todos(item)
|
|
129
|
+
else:
|
|
130
|
+
# self._collect_from_class_attributes(obj, include_submodules, max_depth, current_depth)
|
|
131
|
+
logger.debug(f"Don't know what to do with {obj}")
|
|
132
|
+
|
|
133
|
+
def _check_object_for_todos(self, obj: Any) -> None:
|
|
134
|
+
"""Check if an object has TODO/Done metadata."""
|
|
135
|
+
if hasattr(obj, "todo_meta"):
|
|
136
|
+
if isinstance(obj.todo_meta, TODO):
|
|
137
|
+
logger.info(f"Found todo, by instance and has todo_meta attr on {obj}")
|
|
138
|
+
self.todos.append(obj.todo_meta)
|
|
139
|
+
|
|
140
|
+
def _collect_from_module_attributes(
|
|
141
|
+
self, module: ModuleType | SimpleNamespace, include_submodules: bool, max_depth: int, current_depth: int
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Collect from all attributes of a module.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
module: The module to inspect
|
|
147
|
+
include_submodules: Whether to recursively inspect submodules
|
|
148
|
+
max_depth: Maximum recursion depth for submodules
|
|
149
|
+
current_depth: Current recursion depth
|
|
150
|
+
"""
|
|
151
|
+
if is_stdlib_module(module) or module.__name__ == "builtins":
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
for attr_name in dir(module):
|
|
155
|
+
if attr_name.startswith("__"):
|
|
156
|
+
continue
|
|
157
|
+
# User could put a TODO on a private method and even if it isn't exported, it still is a TODO
|
|
158
|
+
# if attr_name.startswith("_"):
|
|
159
|
+
# continue
|
|
160
|
+
|
|
161
|
+
logger.debug(f"looping : {module} : {attr_name}")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
attr = getattr(module, attr_name)
|
|
165
|
+
|
|
166
|
+
# Handle submodules
|
|
167
|
+
if include_submodules and inspect.ismodule(attr):
|
|
168
|
+
# Avoid circular imports and built-in modules
|
|
169
|
+
if (
|
|
170
|
+
hasattr(attr, "__file__")
|
|
171
|
+
and attr.__file__ is not None
|
|
172
|
+
and not attr.__name__.startswith("builtins")
|
|
173
|
+
):
|
|
174
|
+
self._collect_recursive(attr, include_submodules, max_depth, current_depth + 1)
|
|
175
|
+
# elif isinstance(list, attr) and attr:
|
|
176
|
+
# for item in attr:
|
|
177
|
+
# self._collect_recursive(item, include_submodules, max_depth, current_depth + 1)
|
|
178
|
+
# elif is_stdlib_module(module) or module.__name__ == "builtins":
|
|
179
|
+
# pass
|
|
180
|
+
else:
|
|
181
|
+
logger.debug(f"Collecting something ...{attr_name}: {attr}")
|
|
182
|
+
self._collect_recursive(attr, include_submodules, max_depth, current_depth + 1)
|
|
183
|
+
|
|
184
|
+
except (AttributeError, ImportError, TypeError):
|
|
185
|
+
# Skip attributes that can't be accessed
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
def _collect_from_class_attributes(
|
|
189
|
+
self,
|
|
190
|
+
cls: type | types.FunctionType | types.MethodType,
|
|
191
|
+
include_submodules: bool,
|
|
192
|
+
max_depth: int,
|
|
193
|
+
current_depth: int,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Collect from all attributes of a class.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
cls: The class to inspect
|
|
200
|
+
include_submodules: Whether to recursively inspect submodules
|
|
201
|
+
max_depth: Maximum recursion depth for submodules
|
|
202
|
+
current_depth: Current recursion depth
|
|
203
|
+
"""
|
|
204
|
+
logger.debug("Collecting from class attributes ------------")
|
|
205
|
+
# Check class methods and attributes
|
|
206
|
+
for attr_name in dir(cls):
|
|
207
|
+
if attr_name.startswith("__"):
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
attr = getattr(cls, attr_name)
|
|
212
|
+
self._collect_recursive(attr, include_submodules, max_depth, current_depth + 1)
|
|
213
|
+
except (AttributeError, TypeError):
|
|
214
|
+
logger.error(f"ERROR ON attr_name {attr_name}")
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
def collect_standalone_items(self, items_list: list[TODO]) -> tuple[list[TODO], list[TODO]]:
|
|
218
|
+
"""
|
|
219
|
+
Collect standalone TODO/Done items from a list.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
items_list: List containing TODO and Done instances
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (todos, dones)
|
|
226
|
+
"""
|
|
227
|
+
todos = [item for item in items_list if isinstance(item, TODO)]
|
|
228
|
+
dones = []
|
|
229
|
+
for item in todos:
|
|
230
|
+
if item.is_probably_done():
|
|
231
|
+
dones.append(item)
|
|
232
|
+
todos.remove(item)
|
|
233
|
+
return todos, dones
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def collect_all_todos(
|
|
237
|
+
module: ModuleType,
|
|
238
|
+
standalone_items: list[TODO] | None = None,
|
|
239
|
+
include_submodules: bool = True,
|
|
240
|
+
include_exceptions: bool = True,
|
|
241
|
+
) -> CollectedTODOs:
|
|
242
|
+
"""
|
|
243
|
+
Comprehensive collection of all TODO/Done items and exceptions.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
module: Module to inspect
|
|
247
|
+
standalone_items: List of standalone TODO/Done items
|
|
248
|
+
include_submodules: Whether to inspect submodules
|
|
249
|
+
include_exceptions: Whether to analyze source for TodoExceptions
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary with 'todos', 'dones', and 'exceptions' keys
|
|
253
|
+
"""
|
|
254
|
+
collector = TodoCollector()
|
|
255
|
+
# BUG: _runtime_exceptions is never really used.
|
|
256
|
+
todos, _runtime_exceptions = collector.collect_from_module(module, include_submodules)
|
|
257
|
+
logger.info(f"Found {len(todos)} TODOs in module '{module.__name__}'.")
|
|
258
|
+
|
|
259
|
+
# Collect standalone items if provided
|
|
260
|
+
if standalone_items:
|
|
261
|
+
standalone_todos, standalone_dones = collector.collect_standalone_items(standalone_items)
|
|
262
|
+
logger.info(f"Found {len(standalone_todos)} standalone TODOs and {len(standalone_dones)} standalone Dones.")
|
|
263
|
+
todos.extend(standalone_todos)
|
|
264
|
+
|
|
265
|
+
# Collect exceptions from source analysis
|
|
266
|
+
exceptions = []
|
|
267
|
+
if include_exceptions:
|
|
268
|
+
exception_collector = TodoExceptionCollector()
|
|
269
|
+
exceptions = exception_collector.collect_from_source_analysis(module)
|
|
270
|
+
|
|
271
|
+
return {"todos": todos, "exceptions": exceptions}
|