skylos 1.0.8__py3-none-any.whl → 1.0.9__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.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

skylos/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.8"
3
+ __version__ = "1.0.9"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
skylos/cli.py CHANGED
@@ -24,9 +24,10 @@ class Colors:
24
24
  RESET = '\033[0m'
25
25
  GRAY = '\033[90m'
26
26
 
27
- class ColoredFormatter(logging.Formatter):
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 super().format(record)
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 = ColoredFormatter('%(message)s')
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(formatter)
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("![Dead Code Free](https://img.shields.io/badge/Dead_Code-Free-brightgreen?logo=moleculer&logoColor=white)")
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"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
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
- else:
228
- unused_functions = result.get("unused_functions", [])
229
- unused_imports = result.get("unused_imports", [])
230
-
231
- logger.info(f"\n{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
232
- logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
233
-
234
- logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
235
- logger.info(f" • Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
236
- logger.info(f" • Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
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 args.interactive and (unused_functions or unused_imports):
239
- logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
240
- selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
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 selected_functions or selected_imports:
243
- logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
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 selected_functions:
246
- logger.info(f" Functions: {len(selected_functions)}")
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
- logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
249
-
250
- if selected_imports:
251
- logger.info(f" Imports: {len(selected_imports)}")
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
- logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
254
-
255
- if not args.dry_run:
256
- questions = [
257
- inquirer.Confirm('confirm',
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
- if answers and answers['confirm']:
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}Dry run - no files were modified.{Colors.RESET}")
302
+ logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
285
303
  else:
286
- logger.info(f"\n{Colors.BLUE}No items selected.{Colors.RESET}")
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
- if unused_functions:
290
- logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
291
- logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
292
- for i, item in enumerate(unused_functions, 1):
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("![Dead Code Free](https://img.shields.io/badge/Dead_Code-Free-brightgreen?logo=moleculer&logoColor=white)")
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"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
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
- if unused_functions or unused_imports:
327
- logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
328
- logger.info(f" • Use --interactive to select specific items to remove")
329
- logger.info(f" • Use --dry-run to preview changes before applying them")
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()
skylos/visitor.py CHANGED
@@ -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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -1,7 +1,7 @@
1
- skylos/__init__.py,sha256=DAm7T_QbQzCn5ZQ5ge5C94EP7FvwXvuswMqtbtVJL5Y,151
1
+ skylos/__init__.py,sha256=-Lq8D8LCAJ2XInTPVDyUUxehIExbEV1yHmmEyQcd_3U,151
2
2
  skylos/analyzer.py,sha256=EtL0IIw1NYzdGpXiOFQk4SXTSuTayVGpmGi0mZXL_Hk,7347
3
- skylos/cli.py,sha256=Pq71Gtc3d8E7JVYnEORK_LoLclG96A6b76SIGZh-C6s,13821
4
- skylos/visitor.py,sha256=DvRLXI-N2B6XG7VAB7cXEooD52oG1YNkl86iJbXe2-w,7336
3
+ skylos/cli.py,sha256=l-qfaC0RUH2L9YgjlMOvlQrCPD5hcV3McHIk1az-CI4,13525
4
+ skylos/visitor.py,sha256=uHNHKf7Kf8Qg1sIa-PsH2NHCQD6R9Bd_NELs-41deE8,9339
5
5
  test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  test/compare_tools.py,sha256=0g9PDeJlbst-7hOaQzrL4MiJFQKpqM8q8VeBGzpPczg,22738
7
7
  test/diagnostics.py,sha256=ExuFOCVpc9BDwNYapU96vj9RXLqxji32Sv6wVF4nJYU,13802
@@ -14,8 +14,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
14
14
  test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
15
15
  test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
16
16
  test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
17
- skylos-1.0.8.dist-info/METADATA,sha256=vPL8ZTkKmD4jwre8MV17JmR3hexOzDcamDpG70acaWc,224
18
- skylos-1.0.8.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
19
- skylos-1.0.8.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
20
- skylos-1.0.8.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
21
- skylos-1.0.8.dist-info/RECORD,,
17
+ skylos-1.0.9.dist-info/METADATA,sha256=snCs-osdKIbaJMErLmo7iIbX2mBkbFUxqY_FXZTWNuY,224
18
+ skylos-1.0.9.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
19
+ skylos-1.0.9.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
20
+ skylos-1.0.9.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
21
+ skylos-1.0.9.dist-info/RECORD,,
File without changes