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 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}