skylos 1.0.7__tar.gz → 1.0.9__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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- {skylos-1.0.7 → skylos-1.0.9}/PKG-INFO +1 -1
- {skylos-1.0.7 → skylos-1.0.9}/README.md +3 -0
- {skylos-1.0.7 → skylos-1.0.9}/pyproject.toml +1 -1
- {skylos-1.0.7 → skylos-1.0.9}/setup.py +1 -1
- {skylos-1.0.7 → skylos-1.0.9}/skylos/__init__.py +1 -1
- {skylos-1.0.7 → skylos-1.0.9}/skylos/cli.py +100 -96
- {skylos-1.0.7 → skylos-1.0.9}/skylos/visitor.py +83 -11
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-1.0.7 → skylos-1.0.9}/setup.cfg +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos/analyzer.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/SOURCES.txt +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/requires.txt +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/skylos.egg-info/top_level.txt +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/__init__.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/compare_tools.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/diagnostics.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/__init__.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/app.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/sample_repo/__init__.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/test_skylos.py +0 -0
- {skylos-1.0.7 → skylos-1.0.9}/test/test_visitor.py +0 -0
|
@@ -171,6 +171,9 @@ A: They test common scenarios but can't cover every edge case. Use them as a gui
|
|
|
171
171
|
**Q: Should I automatically delete everything flagged as unused?**
|
|
172
172
|
A: No. Always review results manually, especially for framework code, APIs, and test utilities.
|
|
173
173
|
|
|
174
|
+
**Q: Why did Ruff underperform?**
|
|
175
|
+
A: Like all other tools, Ruff is focused on detecting specific, surface-level issues. Tools like Vulture and Skylos are built SPECIFICALLY for dead code detection. It is NOT a specialized dead code detector. If your goal is dead code, then ruff is the wrong tool. It is a good tool but it's like using a wrench to hammer a nail. Good tool, wrong purpose.
|
|
176
|
+
|
|
174
177
|
## Limitations
|
|
175
178
|
|
|
176
179
|
- **Dynamic code**: `getattr()`, `globals()`, runtime imports are hard to detect
|
|
@@ -24,9 +24,10 @@ class Colors:
|
|
|
24
24
|
RESET = '\033[0m'
|
|
25
25
|
GRAY = '\033[90m'
|
|
26
26
|
|
|
27
|
-
class
|
|
27
|
+
class CleanFormatter(logging.Formatter):
|
|
28
|
+
"""Custom formatter that removes timestamps and log levels for clean output"""
|
|
28
29
|
def format(self, record):
|
|
29
|
-
return
|
|
30
|
+
return record.getMessage()
|
|
30
31
|
|
|
31
32
|
def setup_logger(output_file=None):
|
|
32
33
|
logger = logging.getLogger('skylos')
|
|
@@ -34,17 +35,20 @@ def setup_logger(output_file=None):
|
|
|
34
35
|
|
|
35
36
|
logger.handlers.clear()
|
|
36
37
|
|
|
37
|
-
formatter =
|
|
38
|
+
formatter = CleanFormatter()
|
|
38
39
|
|
|
39
40
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
40
41
|
console_handler.setFormatter(formatter)
|
|
41
42
|
logger.addHandler(console_handler)
|
|
42
43
|
|
|
43
44
|
if output_file:
|
|
45
|
+
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
44
46
|
file_handler = logging.FileHandler(output_file)
|
|
45
|
-
file_handler.setFormatter(
|
|
47
|
+
file_handler.setFormatter(file_formatter)
|
|
46
48
|
logger.addHandler(file_handler)
|
|
47
49
|
|
|
50
|
+
logger.propagate = False
|
|
51
|
+
|
|
48
52
|
return logger
|
|
49
53
|
|
|
50
54
|
def remove_unused_import(file_path: str, import_name: str, line_number: int) -> bool:
|
|
@@ -176,6 +180,21 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
176
180
|
|
|
177
181
|
return selected_functions, selected_imports
|
|
178
182
|
|
|
183
|
+
def print_badge(dead_code_count: int, logger):
|
|
184
|
+
"""Print appropriate badge based on dead code count"""
|
|
185
|
+
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
186
|
+
|
|
187
|
+
if dead_code_count == 0:
|
|
188
|
+
logger.info(f"✨ Your code is 100% dead code free! Add this badge to your README:")
|
|
189
|
+
logger.info("```markdown")
|
|
190
|
+
logger.info("")
|
|
191
|
+
logger.info("```")
|
|
192
|
+
else:
|
|
193
|
+
logger.info(f"Found {dead_code_count} dead code items. Add this badge to your README:")
|
|
194
|
+
logger.info("```markdown")
|
|
195
|
+
logger.info(f"")
|
|
196
|
+
logger.info("```")
|
|
197
|
+
|
|
179
198
|
def main() -> None:
|
|
180
199
|
parser = argparse.ArgumentParser(
|
|
181
200
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
@@ -224,109 +243,94 @@ def main() -> None:
|
|
|
224
243
|
|
|
225
244
|
if args.json:
|
|
226
245
|
logger.info(result_json)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
unused_functions = result.get("unused_functions", [])
|
|
249
|
+
unused_imports = result.get("unused_imports", [])
|
|
250
|
+
|
|
251
|
+
logger.info(f"{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
|
|
252
|
+
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
253
|
+
|
|
254
|
+
logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
|
|
255
|
+
logger.info(f" • Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
|
|
256
|
+
logger.info(f" • Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
|
|
257
|
+
|
|
258
|
+
if args.interactive and (unused_functions or unused_imports):
|
|
259
|
+
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
260
|
+
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
237
261
|
|
|
238
|
-
if
|
|
239
|
-
logger.info(f"\n{Colors.BOLD}
|
|
240
|
-
|
|
262
|
+
if selected_functions or selected_imports:
|
|
263
|
+
logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
|
|
264
|
+
|
|
265
|
+
if selected_functions:
|
|
266
|
+
logger.info(f" Functions: {len(selected_functions)}")
|
|
267
|
+
for func in selected_functions:
|
|
268
|
+
logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
|
|
241
269
|
|
|
242
|
-
if
|
|
243
|
-
logger.info(f"
|
|
270
|
+
if selected_imports:
|
|
271
|
+
logger.info(f" Imports: {len(selected_imports)}")
|
|
272
|
+
for imp in selected_imports:
|
|
273
|
+
logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
|
|
274
|
+
|
|
275
|
+
if not args.dry_run:
|
|
276
|
+
questions = [
|
|
277
|
+
inquirer.Confirm('confirm',
|
|
278
|
+
message="Are you sure you want to remove these items?",
|
|
279
|
+
default=False)
|
|
280
|
+
]
|
|
281
|
+
answers = inquirer.prompt(questions)
|
|
244
282
|
|
|
245
|
-
if
|
|
246
|
-
logger.info(f"
|
|
283
|
+
if answers and answers['confirm']:
|
|
284
|
+
logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
|
|
285
|
+
|
|
247
286
|
for func in selected_functions:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
287
|
+
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
288
|
+
if success:
|
|
289
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed function: {func['name']}")
|
|
290
|
+
else:
|
|
291
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {func['name']}")
|
|
292
|
+
|
|
252
293
|
for imp in selected_imports:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
message="Are you sure you want to remove these items?",
|
|
259
|
-
default=False)
|
|
260
|
-
]
|
|
261
|
-
answers = inquirer.prompt(questions)
|
|
294
|
+
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
295
|
+
if success:
|
|
296
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed import: {imp['name']}")
|
|
297
|
+
else:
|
|
298
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {imp['name']}")
|
|
262
299
|
|
|
263
|
-
|
|
264
|
-
logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
|
|
265
|
-
|
|
266
|
-
for func in selected_functions:
|
|
267
|
-
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
268
|
-
if success:
|
|
269
|
-
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed function: {func['name']}")
|
|
270
|
-
else:
|
|
271
|
-
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {func['name']}")
|
|
272
|
-
|
|
273
|
-
for imp in selected_imports:
|
|
274
|
-
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
275
|
-
if success:
|
|
276
|
-
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed import: {imp['name']}")
|
|
277
|
-
else:
|
|
278
|
-
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {imp['name']}")
|
|
279
|
-
|
|
280
|
-
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
281
|
-
else:
|
|
282
|
-
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
300
|
+
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
283
301
|
else:
|
|
284
|
-
logger.info(f"\n{Colors.YELLOW}
|
|
302
|
+
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
285
303
|
else:
|
|
286
|
-
logger.info(f"\n{Colors.
|
|
304
|
+
logger.info(f"\n{Colors.YELLOW}Dry run - no files were modified.{Colors.RESET}")
|
|
305
|
+
else:
|
|
306
|
+
logger.info(f"\n{Colors.BLUE}No items selected.{Colors.RESET}")
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
if unused_functions:
|
|
310
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
|
|
311
|
+
logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
|
|
312
|
+
for i, item in enumerate(unused_functions, 1):
|
|
313
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
314
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
315
|
+
else:
|
|
316
|
+
logger.info(f"\n{Colors.GREEN}✓ All functions are reachable!{Colors.RESET}")
|
|
287
317
|
|
|
318
|
+
if unused_imports:
|
|
319
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
|
|
320
|
+
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
321
|
+
for i, item in enumerate(unused_imports, 1):
|
|
322
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
323
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
288
324
|
else:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
294
|
-
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
295
|
-
else:
|
|
296
|
-
logger.info(f"\n{Colors.GREEN}✓ All functions are reachable!{Colors.RESET}")
|
|
297
|
-
|
|
298
|
-
if unused_imports:
|
|
299
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
|
|
300
|
-
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
301
|
-
for i, item in enumerate(unused_imports, 1):
|
|
302
|
-
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
303
|
-
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
304
|
-
else:
|
|
305
|
-
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
306
|
-
|
|
307
|
-
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
308
|
-
logger.info(f"{Colors.GRAY}Analysis complete.{Colors.RESET}")
|
|
309
|
-
|
|
310
|
-
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
311
|
-
logger.info(f"{Colors.GRAY}Analysis complete.{Colors.RESET}")
|
|
312
|
-
|
|
313
|
-
dead_code_count = len(unused_functions) + len(unused_imports)
|
|
314
|
-
|
|
315
|
-
if dead_code_count == 0:
|
|
316
|
-
logger.info(f"\n✨ Your code is 100% dead code free! Add this badge to your README:")
|
|
317
|
-
logger.info("```markdown")
|
|
318
|
-
logger.info("")
|
|
319
|
-
logger.info("```")
|
|
320
|
-
else:
|
|
321
|
-
logger.info(f"Found {dead_code_count} dead code items. Add this badge to your README:")
|
|
322
|
-
logger.info("```markdown")
|
|
323
|
-
logger.info(f"")
|
|
324
|
-
logger.info("```")
|
|
325
|
+
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
326
|
+
|
|
327
|
+
dead_code_count = len(unused_functions) + len(unused_imports)
|
|
328
|
+
print_badge(dead_code_count, logger)
|
|
325
329
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
if unused_functions or unused_imports:
|
|
331
|
+
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
332
|
+
logger.info(f" • Use --interactive to select specific items to remove")
|
|
333
|
+
logger.info(f" • Use --dry-run to preview changes before applying them")
|
|
330
334
|
|
|
331
335
|
if __name__ == "__main__":
|
|
332
336
|
main()
|
|
@@ -62,7 +62,23 @@ class Visitor(ast.NodeVisitor):
|
|
|
62
62
|
if n in self.alias:return self.alias[n]
|
|
63
63
|
if n in PYTHON_BUILTINS:return n
|
|
64
64
|
return f"{self.mod}.{n}"if self.mod else n
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
def visit_annotation(self, node):
|
|
67
|
+
if node is not None:
|
|
68
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
69
|
+
self.visit_string_annotation(node.value)
|
|
70
|
+
elif hasattr(node, 's') and isinstance(node.s, str):
|
|
71
|
+
self.visit_string_annotation(node.s)
|
|
72
|
+
else:
|
|
73
|
+
self.visit(node)
|
|
74
|
+
|
|
75
|
+
def visit_string_annotation(self, annotation_str):
|
|
76
|
+
try:
|
|
77
|
+
parsed = ast.parse(annotation_str, mode='eval')
|
|
78
|
+
self.visit(parsed.body)
|
|
79
|
+
except:
|
|
80
|
+
pass
|
|
81
|
+
|
|
66
82
|
def visit_Import(self,node):
|
|
67
83
|
for a in node.names:
|
|
68
84
|
full=a.name
|
|
@@ -81,6 +97,23 @@ class Visitor(ast.NodeVisitor):
|
|
|
81
97
|
self.alias[a.asname or a.name]=full
|
|
82
98
|
self.add_def(full,"import",node.lineno)
|
|
83
99
|
|
|
100
|
+
def visit_arguments(self, args):
|
|
101
|
+
for arg in args.args:
|
|
102
|
+
self.visit_annotation(arg.annotation)
|
|
103
|
+
for arg in args.posonlyargs:
|
|
104
|
+
self.visit_annotation(arg.annotation)
|
|
105
|
+
for arg in args.kwonlyargs:
|
|
106
|
+
self.visit_annotation(arg.annotation)
|
|
107
|
+
if args.vararg:
|
|
108
|
+
self.visit_annotation(args.vararg.annotation)
|
|
109
|
+
if args.kwarg:
|
|
110
|
+
self.visit_annotation(args.kwarg.annotation)
|
|
111
|
+
for default in args.defaults:
|
|
112
|
+
self.visit(default)
|
|
113
|
+
for default in args.kw_defaults:
|
|
114
|
+
if default:
|
|
115
|
+
self.visit(default)
|
|
116
|
+
|
|
84
117
|
def visit_FunctionDef(self,node):
|
|
85
118
|
outer_scope_prefix = '.'.join(self.current_function_scope) + '.' if self.current_function_scope else ''
|
|
86
119
|
|
|
@@ -94,8 +127,13 @@ class Visitor(ast.NodeVisitor):
|
|
|
94
127
|
self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
|
|
95
128
|
|
|
96
129
|
self.current_function_scope.append(node.name)
|
|
130
|
+
|
|
97
131
|
for d_node in node.decorator_list:
|
|
98
132
|
self.visit(d_node)
|
|
133
|
+
|
|
134
|
+
self.visit_arguments(node.args)
|
|
135
|
+
self.visit_annotation(node.returns)
|
|
136
|
+
|
|
99
137
|
for stmt in node.body:
|
|
100
138
|
self.visit(stmt)
|
|
101
139
|
self.current_function_scope.pop()
|
|
@@ -105,10 +143,40 @@ class Visitor(ast.NodeVisitor):
|
|
|
105
143
|
def visit_ClassDef(self,node):
|
|
106
144
|
cname=f"{self.mod}.{node.name}"
|
|
107
145
|
self.add_def(cname,"class",node.lineno)
|
|
146
|
+
|
|
147
|
+
for base in node.bases:
|
|
148
|
+
self.visit(base)
|
|
149
|
+
for keyword in node.keywords:
|
|
150
|
+
self.visit(keyword.value)
|
|
151
|
+
for decorator in node.decorator_list:
|
|
152
|
+
self.visit(decorator)
|
|
153
|
+
|
|
108
154
|
prev=self.cls;self.cls=node.name
|
|
109
155
|
for b in node.body:self.visit(b)
|
|
110
156
|
self.cls=prev
|
|
111
157
|
|
|
158
|
+
def visit_AnnAssign(self, node):
|
|
159
|
+
self.visit_annotation(node.annotation)
|
|
160
|
+
if node.value:
|
|
161
|
+
self.visit(node.value)
|
|
162
|
+
self.visit(node.target)
|
|
163
|
+
|
|
164
|
+
def visit_AugAssign(self, node):
|
|
165
|
+
self.visit(node.target)
|
|
166
|
+
self.visit(node.value)
|
|
167
|
+
|
|
168
|
+
def visit_Subscript(self, node):
|
|
169
|
+
self.visit(node.value)
|
|
170
|
+
self.visit(node.slice)
|
|
171
|
+
|
|
172
|
+
def visit_Slice(self, node):
|
|
173
|
+
if node.lower:
|
|
174
|
+
self.visit(node.lower)
|
|
175
|
+
if node.upper:
|
|
176
|
+
self.visit(node.upper)
|
|
177
|
+
if node.step:
|
|
178
|
+
self.visit(node.step)
|
|
179
|
+
|
|
112
180
|
def visit_Assign(self, node):
|
|
113
181
|
for target in node.targets:
|
|
114
182
|
if isinstance(target, ast.Name) and target.id == "__all__":
|
|
@@ -148,16 +216,6 @@ class Visitor(ast.NodeVisitor):
|
|
|
148
216
|
func_name = parent.slice.value
|
|
149
217
|
self.add_ref(func_name)
|
|
150
218
|
self.add_ref(f"{self.mod}.{func_name}")
|
|
151
|
-
|
|
152
|
-
elif (isinstance(node.func, ast.Attribute) and
|
|
153
|
-
node.func.attr == "format" and
|
|
154
|
-
isinstance(node.func.value, ast.Constant) and
|
|
155
|
-
isinstance(node.func.value.value, str)):
|
|
156
|
-
fmt = node.func.value.value
|
|
157
|
-
if any(isinstance(k.arg, str) and k.arg is None for k in node.keywords):
|
|
158
|
-
for _, n, _, _ in re.findall(r'\{([^}:!]+)', fmt):
|
|
159
|
-
if n:
|
|
160
|
-
self.add_ref(self.qual(n))
|
|
161
219
|
|
|
162
220
|
def visit_Name(self,node):
|
|
163
221
|
if isinstance(node.ctx,ast.Load):
|
|
@@ -169,6 +227,20 @@ class Visitor(ast.NodeVisitor):
|
|
|
169
227
|
if isinstance(node.ctx,ast.Load)and isinstance(node.value,ast.Name):
|
|
170
228
|
self.add_ref(f"{self.qual(node.value.id)}.{node.attr}")
|
|
171
229
|
|
|
230
|
+
def visit_keyword(self, node):
|
|
231
|
+
self.visit(node.value)
|
|
232
|
+
|
|
233
|
+
def visit_withitem(self, node):
|
|
234
|
+
self.visit(node.context_expr)
|
|
235
|
+
if node.optional_vars:
|
|
236
|
+
self.visit(node.optional_vars)
|
|
237
|
+
|
|
238
|
+
def visit_ExceptHandler(self, node):
|
|
239
|
+
if node.type:
|
|
240
|
+
self.visit(node.type)
|
|
241
|
+
for stmt in node.body:
|
|
242
|
+
self.visit(stmt)
|
|
243
|
+
|
|
172
244
|
def generic_visit(self, node):
|
|
173
245
|
for field, value in ast.iter_fields(node):
|
|
174
246
|
if isinstance(value, list):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|