skylos 1.2.2__py3-none-any.whl → 2.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.

Potentially problematic release.


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

skylos/cli.py CHANGED
@@ -2,9 +2,14 @@ import argparse
2
2
  import json
3
3
  import sys
4
4
  import logging
5
- import ast
6
- import skylos
7
- from skylos.analyzer import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
5
+ from skylos.constants import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
6
+ from skylos.server import start_server
7
+ from skylos.analyzer import analyze as run_analyze
8
+ from skylos.codemods import (
9
+ remove_unused_import_cst,
10
+ remove_unused_function_cst,
11
+ )
12
+ import pathlib
8
13
 
9
14
  try:
10
15
  import inquirer
@@ -19,14 +24,11 @@ class Colors:
19
24
  BLUE = '\033[94m'
20
25
  MAGENTA = '\033[95m'
21
26
  CYAN = '\033[96m'
22
- WHITE = '\033[97m'
23
27
  BOLD = '\033[1m'
24
- UNDERLINE = '\033[4m'
25
28
  RESET = '\033[0m'
26
29
  GRAY = '\033[90m'
27
30
 
28
31
  class CleanFormatter(logging.Formatter):
29
- """Custom formatter that removes timestamps and log levels for clean output"""
30
32
  def format(self, record):
31
33
  return record.getMessage()
32
34
 
@@ -52,84 +54,28 @@ def setup_logger(output_file=None):
52
54
 
53
55
  return logger
54
56
 
55
- def remove_unused_import(file_path: str, import_name: str, line_number: int) -> bool:
57
+ def remove_unused_import(file_path, import_name, line_number):
58
+ path = pathlib.Path(file_path)
56
59
  try:
57
- with open(file_path, 'r') as f:
58
- lines = f.readlines()
59
-
60
- line_idx = line_number - 1
61
- original_line = lines[line_idx].strip()
62
-
63
- if original_line.startswith(f'import {import_name}'):
64
- lines[line_idx] = ''
65
- elif original_line.startswith('import ') and f' {import_name}' in original_line:
66
- parts = original_line.split(' ', 1)[1].split(',')
67
- new_parts = [p.strip() for p in parts if p.strip() != import_name]
68
- if new_parts:
69
- lines[line_idx] = f'import {", ".join(new_parts)}\n'
70
- else:
71
- lines[line_idx] = ''
72
- elif original_line.startswith('from ') and import_name in original_line:
73
- if f'import {import_name}' in original_line and ',' not in original_line:
74
- lines[line_idx] = ''
75
- else:
76
- parts = original_line.split('import ', 1)[1].split(',')
77
- new_parts = [p.strip() for p in parts if p.strip() != import_name]
78
- if new_parts:
79
- prefix = original_line.split(' import ')[0]
80
- lines[line_idx] = f'{prefix} import {", ".join(new_parts)}\n'
81
- else:
82
- lines[line_idx] = ''
83
-
84
- with open(file_path, 'w') as f:
85
- f.writelines(lines)
86
-
60
+ src = path.read_text(encoding="utf-8")
61
+ new_code, changed = remove_unused_import_cst(src, import_name, line_number)
62
+ if not changed:
63
+ return False
64
+ path.write_text(new_code, encoding="utf-8")
87
65
  return True
88
66
  except Exception as e:
89
67
  logging.error(f"Failed to remove import {import_name} from {file_path}: {e}")
90
68
  return False
91
69
 
92
- def remove_unused_function(file_path: str, function_name: str, line_number: int) -> bool:
93
- # remove the entire def from the source code
70
+ def remove_unused_function(file_path, function_name, line_number):
71
+ path = pathlib.Path(file_path)
94
72
  try:
95
- with open(file_path, 'r') as f:
96
- content = f.read()
97
-
98
- tree = ast.parse(content)
99
-
100
- lines = content.splitlines()
101
- for node in ast.walk(tree):
102
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
103
- if (node.name in function_name and
104
- node.lineno == line_number):
105
-
106
- start_line = node.lineno - 1
107
-
108
- if node.decorator_list:
109
- start_line = node.decorator_list[0].lineno - 1
110
-
111
- end_line = len(lines)
112
- base_indent = len(lines[start_line]) - len(lines[start_line].lstrip())
113
-
114
- for i in range(node.end_lineno, len(lines)):
115
- if lines[i].strip() == '':
116
- continue
117
- current_indent = len(lines[i]) - len(lines[i].lstrip())
118
- if current_indent <= base_indent and lines[i].strip():
119
- end_line = i
120
- break
121
-
122
- while end_line < len(lines) and lines[end_line].strip() == '':
123
- end_line += 1
124
-
125
- new_lines = lines[:start_line] + lines[end_line:]
126
-
127
- with open(file_path, 'w') as f:
128
- f.write('\n'.join(new_lines) + '\n')
129
-
130
- return True
131
-
132
- return False
73
+ src = path.read_text(encoding="utf-8")
74
+ new_code, changed = remove_unused_function_cst(src, function_name, line_number)
75
+ if not changed:
76
+ return False
77
+ path.write_text(new_code, encoding="utf-8")
78
+ return True
133
79
  except Exception as e:
134
80
  logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
135
81
  return False
@@ -143,9 +89,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
143
89
  selected_imports = []
144
90
 
145
91
  if unused_functions:
146
- logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove:{Colors.RESET}")
92
+ logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove (hit spacebar to select):{Colors.RESET}")
147
93
 
148
94
  function_choices = []
95
+
149
96
  for item in unused_functions:
150
97
  choice_text = f"{item['name']} ({item['file']}:{item['line']})"
151
98
  function_choices.append((choice_text, item))
@@ -162,9 +109,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
162
109
  selected_functions = answers['functions']
163
110
 
164
111
  if unused_imports:
165
- logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove:{Colors.RESET}")
112
+ logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove (hit spacebar to select):{Colors.RESET}")
166
113
 
167
114
  import_choices = []
115
+
168
116
  for item in unused_imports:
169
117
  choice_text = f"{item['name']} ({item['file']}:{item['line']})"
170
118
  import_choices.append((choice_text, item))
@@ -183,11 +131,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
183
131
  return selected_functions, selected_imports
184
132
 
185
133
  def print_badge(dead_code_count: int, logger):
186
- """Print appropriate badge based on dead code count"""
187
134
  logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
188
135
 
189
136
  if dead_code_count == 0:
190
- logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
137
+ logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
191
138
  logger.info("```markdown")
192
139
  logger.info("![Dead Code Free](https://img.shields.io/badge/Dead_Code-Free-brightgreen?logo=moleculer&logoColor=white)")
193
140
  logger.info("```")
@@ -198,25 +145,34 @@ def print_badge(dead_code_count: int, logger):
198
145
  logger.info("```")
199
146
 
200
147
  def main() -> None:
148
+ if len(sys.argv) > 1 and sys.argv[1] == 'run':
149
+ try:
150
+ start_server()
151
+ return
152
+ except ImportError:
153
+ print(f"{Colors.RED}Error: Flask is required {Colors.RESET}")
154
+ print(f"{Colors.YELLOW}Install with: pip install flask flask-cors{Colors.RESET}")
155
+ sys.exit(1)
156
+
201
157
  parser = argparse.ArgumentParser(
202
158
  description="Detect unreachable functions and unused imports in a Python project"
203
159
  )
204
- parser.add_argument("path", help="Path to the Python project to analyze")
160
+ parser.add_argument("path", help="Path to the Python project")
205
161
  parser.add_argument(
206
162
  "--json",
207
163
  action="store_true",
208
- help="Output raw JSON instead of formatted text",
164
+ help="Output raw JSON",
209
165
  )
210
166
  parser.add_argument(
211
167
  "--output",
212
168
  "-o",
213
169
  type=str,
214
- help="Write output to file instead of stdout",
170
+ help="Write output to file",
215
171
  )
216
172
  parser.add_argument(
217
173
  "--verbose", "-v",
218
174
  action="store_true",
219
- help="Enable verbose output"
175
+ help="Enable verbose"
220
176
  )
221
177
  parser.add_argument(
222
178
  "--confidence",
@@ -228,12 +184,12 @@ def main() -> None:
228
184
  parser.add_argument(
229
185
  "--interactive", "-i",
230
186
  action="store_true",
231
- help="Interactively select items to remove (requires inquirer)"
187
+ help="Select items to remove"
232
188
  )
233
189
  parser.add_argument(
234
190
  "--dry-run",
235
191
  action="store_true",
236
- help="Show what would be removed without actually modifying files"
192
+ help="Show what would be removed"
237
193
  )
238
194
 
239
195
  parser.add_argument(
@@ -251,14 +207,14 @@ def main() -> None:
251
207
  dest="include_folders",
252
208
  help="Force include a folder that would otherwise be excluded "
253
209
  "(overrides both default and custom exclusions). "
254
- "Example: --include-folder venv to scan your venv folder."
210
+ "Example: --include-folder venv"
255
211
  )
256
212
 
257
213
  parser.add_argument(
258
214
  "--no-default-excludes",
259
215
  action="store_true",
260
216
  help="Don't exclude default folders (__pycache__, .git, venv, etc.). "
261
- "Only exclude folders specified with --exclude-folder."
217
+ "Only exclude folders with --exclude-folder."
262
218
  )
263
219
 
264
220
  parser.add_argument(
@@ -283,6 +239,7 @@ def main() -> None:
283
239
  if args.verbose:
284
240
  logger.setLevel(logging.DEBUG)
285
241
  logger.debug(f"Analyzing path: {args.path}")
242
+
286
243
  if args.exclude_folders:
287
244
  logger.debug(f"Excluding folders: {args.exclude_folders}")
288
245
 
@@ -300,7 +257,7 @@ def main() -> None:
300
257
  logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
301
258
 
302
259
  try:
303
- result_json = skylos.analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
260
+ result_json = run_analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
304
261
  result = json.loads(result_json)
305
262
 
306
263
  except Exception as e:
@@ -317,15 +274,15 @@ def main() -> None:
317
274
  unused_variables = result.get("unused_variables", [])
318
275
  unused_classes = result.get("unused_classes", [])
319
276
 
320
- logger.info(f"{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
277
+ logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
321
278
  logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
322
279
 
323
280
  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}")
281
+ logger.info(f" * Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
282
+ logger.info(f" * Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
283
+ logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
284
+ logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
285
+ logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
329
286
 
330
287
  if args.interactive and (unused_functions or unused_imports):
331
288
  logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
@@ -358,16 +315,16 @@ def main() -> None:
358
315
  for func in selected_functions:
359
316
  success = remove_unused_function(func['file'], func['name'], func['line'])
360
317
  if success:
361
- logger.info(f" {Colors.GREEN}{Colors.RESET} Removed function: {func['name']}")
318
+ logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
362
319
  else:
363
- logger.error(f" {Colors.RED}{Colors.RESET} Failed to remove: {func['name']}")
320
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
364
321
 
365
322
  for imp in selected_imports:
366
323
  success = remove_unused_import(imp['file'], imp['name'], imp['line'])
367
324
  if success:
368
- logger.info(f" {Colors.GREEN}{Colors.RESET} Removed import: {imp['name']}")
325
+ logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
369
326
  else:
370
- logger.error(f" {Colors.RED}{Colors.RESET} Failed to remove: {imp['name']}")
327
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
371
328
 
372
329
  logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
373
330
  else:
@@ -379,16 +336,16 @@ def main() -> None:
379
336
 
380
337
  else:
381
338
  if unused_functions:
382
- logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
339
+ logger.info(f"\n{Colors.RED}{Colors.BOLD} - Unreachable Functions{Colors.RESET}")
383
340
  logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
384
341
  for i, item in enumerate(unused_functions, 1):
385
342
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
386
343
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
387
344
  else:
388
- logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
345
+ logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
389
346
 
390
347
  if unused_imports:
391
- logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
348
+ logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD} - Unused Imports{Colors.RESET}")
392
349
  logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
393
350
  for i, item in enumerate(unused_imports, 1):
394
351
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
@@ -397,7 +354,7 @@ def main() -> None:
397
354
  logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
398
355
 
399
356
  if unused_parameters:
400
- logger.info(f"\n{Colors.BLUE}{Colors.BOLD}🔧 Unused Parameters{Colors.RESET}")
357
+ logger.info(f"\n{Colors.BLUE}{Colors.BOLD} - Unused Parameters{Colors.RESET}")
401
358
  logger.info(f"{Colors.BLUE}{'=' * 18}{Colors.RESET}")
402
359
  for i, item in enumerate(unused_parameters, 1):
403
360
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.BLUE}{item['name']}{Colors.RESET}")
@@ -406,14 +363,14 @@ def main() -> None:
406
363
  logger.info(f"\n{Colors.GREEN}✓ All parameters are being used!{Colors.RESET}")
407
364
 
408
365
  if unused_variables:
409
- logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}📊 Unused Variables{Colors.RESET}")
366
+ logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Variables{Colors.RESET}")
410
367
  logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
411
368
  for i, item in enumerate(unused_variables, 1):
412
369
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
413
370
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
414
371
 
415
372
  if unused_classes:
416
- logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}📚 Unused Classes{Colors.RESET}")
373
+ logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
417
374
  logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
418
375
  for i, item in enumerate(unused_classes, 1):
419
376
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
@@ -428,9 +385,9 @@ def main() -> None:
428
385
 
429
386
  if unused_functions or unused_imports:
430
387
  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")
388
+ logger.info(f" * Use --select specific items to remove")
389
+ logger.info(f" * Use --dry-run to preview changes")
390
+ logger.info(f" * Use --exclude-folder to skip directories")
434
391
 
435
392
  if __name__ == "__main__":
436
393
  main()
skylos/codemods.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+ import libcst as cst
3
+ from libcst.metadata import PositionProvider
4
+
5
+ def _bound_name_for_import_alias(alias: cst.ImportAlias):
6
+ if alias.asname:
7
+ return alias.asname.name.value
8
+ node = alias.name
9
+ while isinstance(node, cst.Attribute):
10
+ node = node.value
11
+ return node.value
12
+
13
+ class _RemoveImportAtLine(cst.CSTTransformer):
14
+ METADATA_DEPENDENCIES = (PositionProvider,)
15
+
16
+ def __init__(self, target_name, target_line):
17
+ self.target_name = target_name
18
+ self.target_line = target_line
19
+ self.changed = False
20
+
21
+ def _is_target_line(self, node: cst.CSTNode):
22
+ pos = self.get_metadata(PositionProvider, node, None)
23
+ return bool(pos and pos.start.line == self.target_line)
24
+
25
+ def _filter_aliases(self, aliases):
26
+ kept = []
27
+ for alias in aliases:
28
+ bound = _bound_name_for_import_alias(alias)
29
+ if bound == self.target_name:
30
+ self.changed = True
31
+ continue
32
+ kept.append(alias)
33
+ return kept
34
+
35
+ def leave_Import(self, orig: cst.Import, updated: cst.Import):
36
+ if not self._is_target_line(orig):
37
+ return updated
38
+ kept = self._filter_aliases(updated.names)
39
+ if not kept:
40
+ return cst.RemoveFromParent()
41
+ return updated.with_changes(names=tuple(kept))
42
+
43
+ def leave_ImportFrom(self, orig: cst.ImportFrom, updated: cst.ImportFrom):
44
+ if not self._is_target_line(orig):
45
+ return updated
46
+ if isinstance(updated.names, cst.ImportStar):
47
+ return updated
48
+ kept = self._filter_aliases(list(updated.names))
49
+ if not kept:
50
+ return cst.RemoveFromParent()
51
+
52
+ return updated.with_changes(names=tuple(kept))
53
+
54
+ class _RemoveFunctionAtLine(cst.CSTTransformer):
55
+ METADATA_DEPENDENCIES = (PositionProvider,)
56
+
57
+ def __init__(self, func_name, target_line):
58
+ self.func_name = func_name
59
+ self.target_line = target_line
60
+ self.changed = False
61
+
62
+ def _is_target(self, node: cst.CSTNode):
63
+ pos = self.get_metadata(PositionProvider, node, None)
64
+ return bool(pos and pos.start.line == self.target_line)
65
+
66
+ def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
67
+ if self._is_target(orig) and (orig.name.value in self.func_name):
68
+ self.changed = True
69
+ return cst.RemoveFromParent()
70
+ return updated
71
+
72
+ def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
73
+ if self._is_target(orig) and (orig.name.value in self.func_name):
74
+ self.changed = True
75
+ return cst.RemoveFromParent()
76
+
77
+ return updated
78
+
79
+ def remove_unused_import_cst(code, import_name, line_number):
80
+ wrapper = cst.MetadataWrapper(cst.parse_module(code))
81
+ tx = _RemoveImportAtLine(import_name, line_number)
82
+ new_mod = wrapper.visit(tx)
83
+ return new_mod.code, tx.changed
84
+
85
+ def remove_unused_function_cst(code, func_name, line_number):
86
+ wrapper = cst.MetadataWrapper(cst.parse_module(code))
87
+ tx = _RemoveFunctionAtLine(func_name, line_number)
88
+ new_mod = wrapper.visit(tx)
89
+ return new_mod.code, tx.changed
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