skylos 1.2.1__py3-none-any.whl → 2.0.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.

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.22"
3
+ __version__ = "2.0.0"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
skylos/analyzer.py CHANGED
@@ -11,10 +11,6 @@ from skylos.test_aware import TestAwareVisitor
11
11
  import os
12
12
  import traceback
13
13
  from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
14
- import io
15
- import tokenize
16
- import re
17
- import warnings
18
14
 
19
15
  logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
20
16
  logger=logging.getLogger('Skylos')
@@ -34,33 +30,20 @@ def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folde
34
30
 
35
31
  return exclude_set
36
32
 
37
- IGNORE_PATTERNS = (
38
- r"#\s*pragma:\s*no\s+skylos", ## our own pragma
39
- r"#\s*pragma:\s*no\s+cover",
40
- r"#\s*noqa(?:\b|:)", # flake8 style
41
- )
42
-
43
- def _collect_ignored_lines(source: str) -> set[int]:
44
- ignores = set()
45
- for tok in tokenize.generate_tokens(io.StringIO(source).readline):
46
- if tok.type == tokenize.COMMENT:
47
- if any(re.search(pat, tok.string, flags=re.I) for pat in IGNORE_PATTERNS):
48
- ignores.add(tok.start[0])
49
- return ignores
50
-
51
33
  class Skylos:
52
34
  def __init__(self):
53
35
  self.defs={}
54
36
  self.refs=[]
55
37
  self.dynamic=set()
56
38
  self.exports=defaultdict(set)
57
- self.ignored_lines:set[int]=set()
58
39
 
59
40
  def _module(self,root,f):
60
- p=list(f.relative_to(root).parts)
61
- if p[-1].endswith(".py"):p[-1]=p[-1][:-3]
62
- if p[-1]=="__init__":p.pop()
63
- return".".join(p)
41
+ p = list(f.relative_to(root).parts)
42
+ if p[-1].endswith(".py"):
43
+ p[-1] = p[-1][:-3]
44
+ if p[-1] == "__init__":
45
+ p.pop()
46
+ return ".".join(p)
64
47
 
65
48
  def _should_exclude_file(self, file_path, root_path, exclude_folders):
66
49
  if not exclude_folders:
@@ -111,9 +94,9 @@ class Skylos:
111
94
  return all_files, root
112
95
 
113
96
  def _mark_exports(self):
114
- for name, d in self.defs.items():
115
- if d.in_init and not d.simple_name.startswith('_'):
116
- d.is_exported = True
97
+ for name, definition in self.defs.items():
98
+ if definition.in_init and not definition.simple_name.startswith('_'):
99
+ definition.is_exported = True
117
100
 
118
101
  for mod, export_names in self.exports.items():
119
102
  for name in export_names:
@@ -137,8 +120,8 @@ class Skylos:
137
120
  break
138
121
 
139
122
  simple_name_lookup = defaultdict(list)
140
- for d in self.defs.values():
141
- simple_name_lookup[d.simple_name].append(d)
123
+ for definition in self.defs.values():
124
+ simple_name_lookup[definition.simple_name].append(definition)
142
125
 
143
126
  for ref, _ in self.refs:
144
127
  if ref in self.defs:
@@ -151,8 +134,8 @@ class Skylos:
151
134
 
152
135
  simple = ref.split('.')[-1]
153
136
  matches = simple_name_lookup.get(simple, [])
154
- for d in matches:
155
- d.references += 1
137
+ for definition in matches:
138
+ definition.references += 1
156
139
 
157
140
  for module_name in self.dynamic:
158
141
  for def_name, def_obj in self.defs.items():
@@ -172,59 +155,58 @@ class Skylos:
172
155
  return []
173
156
 
174
157
  def _apply_penalties(self, def_obj, visitor, framework):
175
- c = 100
176
-
158
+ confidence=100
177
159
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
178
- c -= PENALTIES["private_name"]
160
+ confidence -= PENALTIES["private_name"]
179
161
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
180
- c -= PENALTIES["dunder_or_magic"]
162
+ confidence -= PENALTIES["dunder_or_magic"]
181
163
  if def_obj.type == "variable" and def_obj.simple_name == "_":
182
- c -= PENALTIES["underscored_var"]
164
+ confidence -= PENALTIES["underscored_var"]
183
165
  if def_obj.in_init and def_obj.type in ("function", "class"):
184
- c -= PENALTIES["in_init_file"]
166
+ confidence -= PENALTIES["in_init_file"]
185
167
  if def_obj.name.split(".")[0] in self.dynamic:
186
- c -= PENALTIES["dynamic_module"]
168
+ confidence -= PENALTIES["dynamic_module"]
187
169
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
188
- c -= PENALTIES["test_related"]
170
+ confidence -= PENALTIES["test_related"]
189
171
 
190
172
  framework_confidence = detect_framework_usage(def_obj, visitor=framework)
191
173
  if framework_confidence is not None:
192
- c = min(c, framework_confidence)
174
+ confidence = min(confidence, framework_confidence)
193
175
 
194
176
  if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
195
- c = 0
177
+ confidence = 0
196
178
 
197
179
  if def_obj.type == "parameter":
198
180
  if def_obj.simple_name in ("self", "cls"):
199
- c = 0
181
+ confidence = 0
200
182
  elif "." in def_obj.name:
201
183
  method_name = def_obj.name.split(".")[-2]
202
184
  if method_name.startswith("__") and method_name.endswith("__"):
203
- c = 0
185
+ confidence = 0
204
186
 
205
187
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
206
- c = 0
188
+ confidence = 0
207
189
 
208
190
  if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
209
191
  def_obj.simple_name in ("annotations", "absolute_import", "division",
210
192
  "print_function", "unicode_literals", "generator_stop")):
211
- c = 0
193
+ confidence = 0
212
194
 
213
- def_obj.confidence = max(c, 0)
195
+ def_obj.confidence = max(confidence, 0)
214
196
 
215
197
  def _apply_heuristics(self):
216
198
  class_methods = defaultdict(list)
217
- for d in self.defs.values():
218
- if d.type in ("method", "function") and "." in d.name:
219
- cls = d.name.rsplit(".", 1)[0]
199
+ for definition in self.defs.values():
200
+ if definition.type in ("method", "function") and "." in definition.name:
201
+ cls = definition.name.rsplit(".", 1)[0]
220
202
  if cls in self.defs and self.defs[cls].type == "class":
221
- class_methods[cls].append(d)
203
+ class_methods[cls].append(definition)
222
204
 
223
205
  for cls, methods in class_methods.items():
224
206
  if self.defs[cls].references > 0:
225
- for m in methods:
226
- if m.simple_name in AUTO_CALLED: # __init__, __enter__, __exit__
227
- m.references += 1
207
+ for method in methods:
208
+ if method.simple_name in AUTO_CALLED:
209
+ method.references += 1
228
210
 
229
211
  def analyze(self, path, thr=60, exclude_folders=None):
230
212
  files, root = self._get_python_files(path, exclude_folders)
@@ -251,25 +233,11 @@ class Skylos:
251
233
 
252
234
  for file in files:
253
235
  mod = modmap[file]
236
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
254
237
 
255
- result = proc_file(file, mod)
256
-
257
- if len(result) == 7: ##new
258
- defs, refs, dyn, exports, test_flags, framework_flags, ignored = result
259
- self.ignored_lines.update(ignored)
260
- else: ##legacy
261
- warnings.warn(
262
- "proc_file() now returns 7 values (added ignored_lines). "
263
- "The 6-value form is deprecated and will disappear.",
264
- DeprecationWarning,
265
- stacklevel=2,
266
- )
267
- defs, refs, dyn, exports, test_flags, framework_flags = result
268
-
269
- # apply penalties while we still have the file-specific flags
270
- for d in defs:
271
- self._apply_penalties(d, test_flags, framework_flags)
272
- self.defs[d.name] = d
238
+ for definition in defs:
239
+ self._apply_penalties(definition, test_flags, framework_flags)
240
+ self.defs[definition.name] = definition
273
241
 
274
242
  self.refs.extend(refs)
275
243
  self.dynamic.update(dyn)
@@ -282,11 +250,9 @@ class Skylos:
282
250
  thr = max(0, thr)
283
251
 
284
252
  unused = []
285
- for d in self.defs.values():
286
- if (d.references == 0 and not d.is_exported
287
- and d.confidence >= thr
288
- and d.line not in self.ignored_lines):
289
- unused.append(d.to_dict())
253
+ for definition in self.defs.values():
254
+ if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
255
+ unused.append(definition.to_dict())
290
256
 
291
257
  result = {
292
258
  "unused_functions": [],
@@ -322,7 +288,6 @@ def proc_file(file_or_args, mod=None):
322
288
 
323
289
  try:
324
290
  source = Path(file).read_text(encoding="utf-8")
325
- ignored = _collect_ignored_lines(source)
326
291
  tree = ast.parse(source)
327
292
 
328
293
  tv = TestAwareVisitor(filename=file)
@@ -334,7 +299,7 @@ def proc_file(file_or_args, mod=None):
334
299
  v = Visitor(mod, file)
335
300
  v.visit(tree)
336
301
 
337
- return v.defs, v.refs, v.dyn, v.exports, tv, fv, ignored
302
+ return v.defs, v.refs, v.dyn, v.exports, tv, fv
338
303
  except Exception as e:
339
304
  logger.error(f"{file}: {e}")
340
305
  if os.getenv("SKYLOS_DEBUG"):
@@ -342,55 +307,56 @@ def proc_file(file_or_args, mod=None):
342
307
  dummy_visitor = TestAwareVisitor(filename=file)
343
308
  dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
344
309
 
345
- return [], [], set(), set(), dummy_visitor, dummy_framework_visitor, set()
310
+ return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
346
311
 
347
312
  def analyze(path,conf=60, exclude_folders=None):
348
313
  return Skylos().analyze(path,conf, exclude_folders)
349
314
 
350
- if __name__=="__main__":
315
+ if __name__ == "__main__":
351
316
  if len(sys.argv)>1:
352
- p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
353
- result = analyze(p,c)
317
+ p = sys.argv[1]
318
+ confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
319
+ result = analyze(p,confidence)
354
320
 
355
321
  data = json.loads(result)
356
- print("\n🔍 Python Static Analysis Results")
322
+ print("\n Python Static Analysis Results")
357
323
  print("===================================\n")
358
324
 
359
325
  total_items = sum(len(items) for items in data.values())
360
326
 
361
327
  print("Summary:")
362
328
  if data["unused_functions"]:
363
- print(f" Unreachable functions: {len(data['unused_functions'])}")
329
+ print(f" * Unreachable functions: {len(data['unused_functions'])}")
364
330
  if data["unused_imports"]:
365
- print(f" Unused imports: {len(data['unused_imports'])}")
331
+ print(f" * Unused imports: {len(data['unused_imports'])}")
366
332
  if data["unused_classes"]:
367
- print(f" Unused classes: {len(data['unused_classes'])}")
333
+ print(f" * Unused classes: {len(data['unused_classes'])}")
368
334
  if data["unused_variables"]:
369
- print(f" Unused variables: {len(data['unused_variables'])}")
335
+ print(f" * Unused variables: {len(data['unused_variables'])}")
370
336
 
371
337
  if data["unused_functions"]:
372
- print("\n📦 Unreachable Functions")
338
+ print("\n - Unreachable Functions")
373
339
  print("=======================")
374
340
  for i, func in enumerate(data["unused_functions"], 1):
375
341
  print(f" {i}. {func['name']}")
376
342
  print(f" └─ {func['file']}:{func['line']}")
377
343
 
378
344
  if data["unused_imports"]:
379
- print("\n📥 Unused Imports")
345
+ print("\n - Unused Imports")
380
346
  print("================")
381
347
  for i, imp in enumerate(data["unused_imports"], 1):
382
348
  print(f" {i}. {imp['simple_name']}")
383
349
  print(f" └─ {imp['file']}:{imp['line']}")
384
350
 
385
351
  if data["unused_classes"]:
386
- print("\n📋 Unused Classes")
352
+ print("\n - Unused Classes")
387
353
  print("=================")
388
354
  for i, cls in enumerate(data["unused_classes"], 1):
389
355
  print(f" {i}. {cls['name']}")
390
356
  print(f" └─ {cls['file']}:{cls['line']}")
391
357
 
392
358
  if data["unused_variables"]:
393
- print("\n📊 Unused Variables")
359
+ print("\n - Unused Variables")
394
360
  print("==================")
395
361
  for i, var in enumerate(data["unused_variables"], 1):
396
362
  print(f" {i}. {var['name']}")
@@ -403,7 +369,7 @@ if __name__=="__main__":
403
369
  print(f"```")
404
370
 
405
371
  print("\nNext steps:")
406
- print(" Use --interactive to select specific items to remove")
407
- print(" Use --dry-run to preview changes before applying them")
372
+ print(" * Use --interactive to select specific items to remove")
373
+ print(" * Use --dry-run to preview changes before applying them")
408
374
  else:
409
375
  print("Usage: python Skylos.py <path> [confidence_threshold]")
skylos/cli.py CHANGED
@@ -4,7 +4,8 @@ import sys
4
4
  import logging
5
5
  import ast
6
6
  import skylos
7
- from skylos.analyzer import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
7
+ from skylos.constants import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
8
+ from skylos.server import start_server
8
9
 
9
10
  try:
10
11
  import inquirer
@@ -19,14 +20,11 @@ class Colors:
19
20
  BLUE = '\033[94m'
20
21
  MAGENTA = '\033[95m'
21
22
  CYAN = '\033[96m'
22
- WHITE = '\033[97m'
23
23
  BOLD = '\033[1m'
24
- UNDERLINE = '\033[4m'
25
24
  RESET = '\033[0m'
26
25
  GRAY = '\033[90m'
27
26
 
28
27
  class CleanFormatter(logging.Formatter):
29
- """Custom formatter that removes timestamps and log levels for clean output"""
30
28
  def format(self, record):
31
29
  return record.getMessage()
32
30
 
@@ -62,6 +60,7 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
62
60
 
63
61
  if original_line.startswith(f'import {import_name}'):
64
62
  lines[line_idx] = ''
63
+
65
64
  elif original_line.startswith('import ') and f' {import_name}' in original_line:
66
65
  parts = original_line.split(' ', 1)[1].split(',')
67
66
  new_parts = [p.strip() for p in parts if p.strip() != import_name]
@@ -69,6 +68,7 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
69
68
  lines[line_idx] = f'import {", ".join(new_parts)}\n'
70
69
  else:
71
70
  lines[line_idx] = ''
71
+
72
72
  elif original_line.startswith('from ') and import_name in original_line:
73
73
  if f'import {import_name}' in original_line and ',' not in original_line:
74
74
  lines[line_idx] = ''
@@ -90,7 +90,6 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
90
90
  return False
91
91
 
92
92
  def remove_unused_function(file_path: str, function_name: str, line_number: int) -> bool:
93
- # remove the entire def from the source code
94
93
  try:
95
94
  with open(file_path, 'r') as f:
96
95
  content = f.read()
@@ -130,8 +129,8 @@ def remove_unused_function(file_path: str, function_name: str, line_number: int)
130
129
  return True
131
130
 
132
131
  return False
133
- except Exception as e:
134
- logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
132
+ except:
133
+ logging.error(f"Failed to remove function {function_name}")
135
134
  return False
136
135
 
137
136
  def interactive_selection(logger, unused_functions, unused_imports):
@@ -146,6 +145,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
146
145
  logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove:{Colors.RESET}")
147
146
 
148
147
  function_choices = []
148
+
149
149
  for item in unused_functions:
150
150
  choice_text = f"{item['name']} ({item['file']}:{item['line']})"
151
151
  function_choices.append((choice_text, item))
@@ -165,6 +165,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
165
165
  logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove:{Colors.RESET}")
166
166
 
167
167
  import_choices = []
168
+
168
169
  for item in unused_imports:
169
170
  choice_text = f"{item['name']} ({item['file']}:{item['line']})"
170
171
  import_choices.append((choice_text, item))
@@ -183,11 +184,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
183
184
  return selected_functions, selected_imports
184
185
 
185
186
  def print_badge(dead_code_count: int, logger):
186
- """Print appropriate badge based on dead code count"""
187
187
  logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
188
188
 
189
189
  if dead_code_count == 0:
190
- logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
190
+ logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
191
191
  logger.info("```markdown")
192
192
  logger.info("![Dead Code Free](https://img.shields.io/badge/Dead_Code-Free-brightgreen?logo=moleculer&logoColor=white)")
193
193
  logger.info("```")
@@ -198,25 +198,34 @@ def print_badge(dead_code_count: int, logger):
198
198
  logger.info("```")
199
199
 
200
200
  def main() -> None:
201
+ if len(sys.argv) > 1 and sys.argv[1] == 'run':
202
+ try:
203
+ start_server()
204
+ return
205
+ except ImportError:
206
+ print(f"{Colors.RED}Error: Flask is required {Colors.RESET}")
207
+ print(f"{Colors.YELLOW}Install with: pip install flask flask-cors{Colors.RESET}")
208
+ sys.exit(1)
209
+
201
210
  parser = argparse.ArgumentParser(
202
211
  description="Detect unreachable functions and unused imports in a Python project"
203
212
  )
204
- parser.add_argument("path", help="Path to the Python project to analyze")
213
+ parser.add_argument("path", help="Path to the Python project")
205
214
  parser.add_argument(
206
215
  "--json",
207
216
  action="store_true",
208
- help="Output raw JSON instead of formatted text",
217
+ help="Output raw JSON",
209
218
  )
210
219
  parser.add_argument(
211
220
  "--output",
212
221
  "-o",
213
222
  type=str,
214
- help="Write output to file instead of stdout",
223
+ help="Write output to file",
215
224
  )
216
225
  parser.add_argument(
217
226
  "--verbose", "-v",
218
227
  action="store_true",
219
- help="Enable verbose output"
228
+ help="Enable verbose"
220
229
  )
221
230
  parser.add_argument(
222
231
  "--confidence",
@@ -228,12 +237,12 @@ def main() -> None:
228
237
  parser.add_argument(
229
238
  "--interactive", "-i",
230
239
  action="store_true",
231
- help="Interactively select items to remove (requires inquirer)"
240
+ help="Select items to remove"
232
241
  )
233
242
  parser.add_argument(
234
243
  "--dry-run",
235
244
  action="store_true",
236
- help="Show what would be removed without actually modifying files"
245
+ help="Show what would be removed"
237
246
  )
238
247
 
239
248
  parser.add_argument(
@@ -251,14 +260,14 @@ def main() -> None:
251
260
  dest="include_folders",
252
261
  help="Force include a folder that would otherwise be excluded "
253
262
  "(overrides both default and custom exclusions). "
254
- "Example: --include-folder venv to scan your venv folder."
263
+ "Example: --include-folder venv"
255
264
  )
256
265
 
257
266
  parser.add_argument(
258
267
  "--no-default-excludes",
259
268
  action="store_true",
260
269
  help="Don't exclude default folders (__pycache__, .git, venv, etc.). "
261
- "Only exclude folders specified with --exclude-folder."
270
+ "Only exclude folders with --exclude-folder."
262
271
  )
263
272
 
264
273
  parser.add_argument(
@@ -283,6 +292,7 @@ def main() -> None:
283
292
  if args.verbose:
284
293
  logger.setLevel(logging.DEBUG)
285
294
  logger.debug(f"Analyzing path: {args.path}")
295
+
286
296
  if args.exclude_folders:
287
297
  logger.debug(f"Excluding folders: {args.exclude_folders}")
288
298
 
@@ -317,15 +327,15 @@ def main() -> None:
317
327
  unused_variables = result.get("unused_variables", [])
318
328
  unused_classes = result.get("unused_classes", [])
319
329
 
320
- logger.info(f"{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
330
+ logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
321
331
  logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
322
332
 
323
333
  logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
324
- logger.info(f" Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
325
- logger.info(f" Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
326
- logger.info(f" Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
327
- logger.info(f" Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
328
- logger.info(f" Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
334
+ logger.info(f" * Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
335
+ logger.info(f" * Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
336
+ logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
337
+ logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
338
+ logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
329
339
 
330
340
  if args.interactive and (unused_functions or unused_imports):
331
341
  logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
@@ -358,16 +368,16 @@ def main() -> None:
358
368
  for func in selected_functions:
359
369
  success = remove_unused_function(func['file'], func['name'], func['line'])
360
370
  if success:
361
- logger.info(f" {Colors.GREEN}{Colors.RESET} Removed function: {func['name']}")
371
+ logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
362
372
  else:
363
- logger.error(f" {Colors.RED}{Colors.RESET} Failed to remove: {func['name']}")
373
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
364
374
 
365
375
  for imp in selected_imports:
366
376
  success = remove_unused_import(imp['file'], imp['name'], imp['line'])
367
377
  if success:
368
- logger.info(f" {Colors.GREEN}{Colors.RESET} Removed import: {imp['name']}")
378
+ logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
369
379
  else:
370
- logger.error(f" {Colors.RED}{Colors.RESET} Failed to remove: {imp['name']}")
380
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
371
381
 
372
382
  logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
373
383
  else:
@@ -379,16 +389,16 @@ def main() -> None:
379
389
 
380
390
  else:
381
391
  if unused_functions:
382
- logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
392
+ logger.info(f"\n{Colors.RED}{Colors.BOLD} - Unreachable Functions{Colors.RESET}")
383
393
  logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
384
394
  for i, item in enumerate(unused_functions, 1):
385
395
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
386
396
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
387
397
  else:
388
- logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
398
+ logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
389
399
 
390
400
  if unused_imports:
391
- logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
401
+ logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD} - Unused Imports{Colors.RESET}")
392
402
  logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
393
403
  for i, item in enumerate(unused_imports, 1):
394
404
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
@@ -397,7 +407,7 @@ def main() -> None:
397
407
  logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
398
408
 
399
409
  if unused_parameters:
400
- logger.info(f"\n{Colors.BLUE}{Colors.BOLD}🔧 Unused Parameters{Colors.RESET}")
410
+ logger.info(f"\n{Colors.BLUE}{Colors.BOLD} - Unused Parameters{Colors.RESET}")
401
411
  logger.info(f"{Colors.BLUE}{'=' * 18}{Colors.RESET}")
402
412
  for i, item in enumerate(unused_parameters, 1):
403
413
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.BLUE}{item['name']}{Colors.RESET}")
@@ -406,14 +416,14 @@ def main() -> None:
406
416
  logger.info(f"\n{Colors.GREEN}✓ All parameters are being used!{Colors.RESET}")
407
417
 
408
418
  if unused_variables:
409
- logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}📊 Unused Variables{Colors.RESET}")
419
+ logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Variables{Colors.RESET}")
410
420
  logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
411
421
  for i, item in enumerate(unused_variables, 1):
412
422
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
413
423
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
414
424
 
415
425
  if unused_classes:
416
- logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}📚 Unused Classes{Colors.RESET}")
426
+ logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
417
427
  logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
418
428
  for i, item in enumerate(unused_classes, 1):
419
429
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
@@ -428,9 +438,9 @@ def main() -> None:
428
438
 
429
439
  if unused_functions or unused_imports:
430
440
  logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
431
- logger.info(f" Use --interactive to select specific items to remove")
432
- logger.info(f" Use --dry-run to preview changes before applying them")
433
- logger.info(f" Use --exclude-folder to skip directories like node_modules, .git")
441
+ logger.info(f" * Use --select specific items to remove")
442
+ logger.info(f" * Use --dry-run to preview changes")
443
+ logger.info(f" * Use --exclude-folder to skip directories")
434
444
 
435
445
  if __name__ == "__main__":
436
446
  main()
skylos/constants.py CHANGED
@@ -2,18 +2,18 @@ import re
2
2
  from pathlib import Path
3
3
 
4
4
  PENALTIES = {
5
- "private_name": 80,
6
- "dunder_or_magic": 100,
7
- "underscored_var": 100,
8
- "in_init_file": 15,
9
- "dynamic_module": 40,
10
- "test_related": 100,
11
- "framework_magic": 40,
5
+ "private_name": 80,
6
+ "dunder_or_magic": 100,
7
+ "underscored_var": 100,
8
+ "in_init_file": 15,
9
+ "dynamic_module": 40,
10
+ "test_related": 100,
11
+ "framework_magic": 40,
12
12
  }
13
13
 
14
- TEST_FILE_RE = re.compile(r"(?:^|[/\\])tests?[/\\]|_test\.py$", re.I)
14
+ TEST_FILE_RE = re.compile(r"(?:^|[/\\])tests?[/\\]|_test\.py$", re.I)
15
15
  TEST_IMPORT_RE = re.compile(r"^(pytest|unittest|nose|mock|responses)(\.|$)")
16
- TEST_DECOR_RE = re.compile(r"""^(
16
+ TEST_DECOR_RE = re.compile(r"""^(
17
17
  pytest\.(fixture|mark) |
18
18
  patch(\.|$) |
19
19
  responses\.activate |
@@ -39,4 +39,19 @@ def is_test_path(p: Path | str) -> bool:
39
39
  return bool(TEST_FILE_RE.search(str(p)))
40
40
 
41
41
  def is_framework_path(p: Path | str) -> bool:
42
- return bool(FRAMEWORK_FILE_RE.search(str(p)))
42
+ return bool(FRAMEWORK_FILE_RE.search(str(p)))
43
+
44
+ def parse_exclude_folders(user_exclude_folders= None, use_defaults= True, include_folders= None):
45
+ exclude_folders = set()
46
+
47
+ if use_defaults:
48
+ exclude_folders.update(DEFAULT_EXCLUDE_FOLDERS)
49
+
50
+ if user_exclude_folders:
51
+ exclude_folders.update(user_exclude_folders)
52
+
53
+ if include_folders:
54
+ for folder in include_folders:
55
+ exclude_folders.discard(folder)
56
+
57
+ return exclude_folders