skylos 2.1.1__tar.gz → 2.1.2__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.

Files changed (42) hide show
  1. {skylos-2.1.1 → skylos-2.1.2}/PKG-INFO +1 -1
  2. {skylos-2.1.1 → skylos-2.1.2}/pyproject.toml +1 -1
  3. {skylos-2.1.1 → skylos-2.1.2}/setup.py +1 -1
  4. {skylos-2.1.1 → skylos-2.1.2}/skylos/__init__.py +1 -1
  5. {skylos-2.1.1 → skylos-2.1.2}/skylos/analyzer.py +34 -4
  6. {skylos-2.1.1 → skylos-2.1.2}/skylos/cli.py +13 -3
  7. {skylos-2.1.1 → skylos-2.1.2}/skylos/constants.py +2 -3
  8. {skylos-2.1.1 → skylos-2.1.2}/skylos/visitor.py +97 -24
  9. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/PKG-INFO +1 -1
  10. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/SOURCES.txt +1 -0
  11. {skylos-2.1.1 → skylos-2.1.2}/test/test_analyzer.py +6 -43
  12. skylos-2.1.2/test/test_new_behaviours.py +52 -0
  13. {skylos-2.1.1 → skylos-2.1.2}/README.md +0 -0
  14. {skylos-2.1.1 → skylos-2.1.2}/setup.cfg +0 -0
  15. {skylos-2.1.1 → skylos-2.1.2}/skylos/codemods.py +0 -0
  16. {skylos-2.1.1 → skylos-2.1.2}/skylos/framework_aware.py +0 -0
  17. {skylos-2.1.1 → skylos-2.1.2}/skylos/server.py +0 -0
  18. {skylos-2.1.1 → skylos-2.1.2}/skylos/test_aware.py +0 -0
  19. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/dependency_links.txt +0 -0
  20. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/entry_points.txt +0 -0
  21. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/requires.txt +0 -0
  22. {skylos-2.1.1 → skylos-2.1.2}/skylos.egg-info/top_level.txt +0 -0
  23. {skylos-2.1.1 → skylos-2.1.2}/test/__init__.py +0 -0
  24. {skylos-2.1.1 → skylos-2.1.2}/test/compare_tools.py +0 -0
  25. {skylos-2.1.1 → skylos-2.1.2}/test/conftest.py +0 -0
  26. {skylos-2.1.1 → skylos-2.1.2}/test/diagnostics.py +0 -0
  27. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/__init__.py +0 -0
  28. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/app.py +0 -0
  29. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/sample_repo/__init__.py +0 -0
  30. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/sample_repo/commands.py +0 -0
  31. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/sample_repo/models.py +0 -0
  32. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/sample_repo/routes.py +0 -0
  33. {skylos-2.1.1 → skylos-2.1.2}/test/sample_repo/sample_repo/utils.py +0 -0
  34. {skylos-2.1.1 → skylos-2.1.2}/test/test_changes_analyzer.py +0 -0
  35. {skylos-2.1.1 → skylos-2.1.2}/test/test_cli.py +0 -0
  36. {skylos-2.1.1 → skylos-2.1.2}/test/test_codemods.py +0 -0
  37. {skylos-2.1.1 → skylos-2.1.2}/test/test_constants.py +0 -0
  38. {skylos-2.1.1 → skylos-2.1.2}/test/test_framework_aware.py +0 -0
  39. {skylos-2.1.1 → skylos-2.1.2}/test/test_integration.py +0 -0
  40. {skylos-2.1.1 → skylos-2.1.2}/test/test_skylos.py +0 -0
  41. {skylos-2.1.1 → skylos-2.1.2}/test/test_test_aware.py +0 -0
  42. {skylos-2.1.1 → skylos-2.1.2}/test/test_visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skylos"
7
- version = "2.1.1"
7
+ version = "2.1.2"
8
8
  requires-python = ">=3.9"
9
9
  description = "A static analysis tool for Python codebases"
10
10
  authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="skylos",
5
- version="2.1.1",
5
+ version="2.1.2",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
8
  install_requires=[
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "2.1.1"
3
+ __version__ = "2.1.2"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
@@ -155,18 +155,40 @@ class Skylos:
155
155
 
156
156
  def _apply_penalties(self, def_obj, visitor, framework):
157
157
  confidence=100
158
+
159
+ if getattr(visitor, "ignore_lines", None) and def_obj.line in visitor.ignore_lines:
160
+ def_obj.confidence = 0
161
+ return
162
+
163
+ if def_obj.type == "variable" and def_obj.simple_name == "_":
164
+ def_obj.confidence = 0
165
+ return
166
+
158
167
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
159
168
  confidence -= PENALTIES["private_name"]
169
+
160
170
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
161
171
  confidence -= PENALTIES["dunder_or_magic"]
162
- if def_obj.type == "variable" and def_obj.simple_name.isupper():
163
- confidence = 0
172
+
164
173
  if def_obj.in_init and def_obj.type in ("function", "class"):
165
174
  confidence -= PENALTIES["in_init_file"]
175
+
166
176
  if def_obj.name.split(".")[0] in self.dynamic:
167
177
  confidence -= PENALTIES["dynamic_module"]
178
+
168
179
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
169
180
  confidence -= PENALTIES["test_related"]
181
+
182
+ if def_obj.type == "variable" and getattr(framework, "dataclass_fields", None):
183
+ if def_obj.name in framework.dataclass_fields:
184
+ def_obj.confidence = 0
185
+ return
186
+
187
+ if def_obj.type == "variable":
188
+ fr = getattr(framework, "first_read_lineno", {}).get(def_obj.name)
189
+ if fr is not None and fr >= def_obj.line:
190
+ def_obj.confidence = 0
191
+ return
170
192
 
171
193
  framework_confidence = detect_framework_usage(def_obj, visitor=framework)
172
194
  if framework_confidence is not None:
@@ -262,7 +284,7 @@ class Skylos:
262
284
  for d in sorted(self.defs.values(), key=def_sort_key):
263
285
  if shown >= 50:
264
286
  break
265
- print(f" {d.type:<8} refs={d.references:<2} conf={d.confidence:<3} exported={d.is_exported} line={d.line:<4} {d.name}")
287
+ print(f" type={d.type} refs={d.references} conf={d.confidence} exported={d.is_exported} line={d.line} name={d.name}")
266
288
  shown += 1
267
289
 
268
290
  unused = []
@@ -304,10 +326,13 @@ def proc_file(file_or_args, mod=None):
304
326
 
305
327
  try:
306
328
  source = Path(file).read_text(encoding="utf-8")
307
- tree = ast.parse(source)
329
+ ignore_lines = {i for i, line in enumerate(source.splitlines(), start=1)
330
+ if "pragma: no skylos" in line}
331
+ tree = ast.parse(source)
308
332
 
309
333
  tv = TestAwareVisitor(filename=file)
310
334
  tv.visit(tree)
335
+ tv.ignore_lines = ignore_lines
311
336
 
312
337
  fv = FrameworkAwareVisitor(filename=file)
313
338
  fv.visit(tree)
@@ -315,12 +340,17 @@ def proc_file(file_or_args, mod=None):
315
340
  v = Visitor(mod, file)
316
341
  v.visit(tree)
317
342
 
343
+ fv.dataclass_fields = getattr(v, "dataclass_fields", set())
344
+ fv.first_read_lineno = getattr(v, "first_read_lineno", {})
345
+
318
346
  return v.defs, v.refs, v.dyn, v.exports, tv, fv
347
+
319
348
  except Exception as e:
320
349
  logger.error(f"{file}: {e}")
321
350
  if os.getenv("SKYLOS_DEBUG"):
322
351
  logger.error(traceback.format_exc())
323
352
  dummy_visitor = TestAwareVisitor(filename=file)
353
+ dummy_visitor.ignore_lines = set()
324
354
  dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
325
355
 
326
356
  return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
@@ -10,6 +10,7 @@ from skylos.codemods import (
10
10
  remove_unused_function_cst,
11
11
  )
12
12
  import pathlib
13
+ import skylos
13
14
 
14
15
  try:
15
16
  import inquirer
@@ -144,7 +145,7 @@ def print_badge(dead_code_count: int, logger):
144
145
  logger.info(f"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
145
146
  logger.info("```")
146
147
 
147
- def main() -> None:
148
+ def main():
148
149
  if len(sys.argv) > 1 and sys.argv[1] == 'run':
149
150
  try:
150
151
  start_server()
@@ -158,6 +159,14 @@ def main() -> None:
158
159
  description="Detect unreachable functions and unused imports in a Python project"
159
160
  )
160
161
  parser.add_argument("path", help="Path to the Python project")
162
+
163
+ parser.add_argument(
164
+ "--version",
165
+ action="version",
166
+ version=f"skylos {skylos.__version__}",
167
+ help="Show version and exit"
168
+ )
169
+
161
170
  parser.add_argument(
162
171
  "--json",
163
172
  action="store_true",
@@ -368,6 +377,8 @@ def main() -> None:
368
377
  for i, item in enumerate(unused_variables, 1):
369
378
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
370
379
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
380
+ else:
381
+ logger.info(f"\n{Colors.GREEN}✓ All variables are being used!{Colors.RESET}")
371
382
 
372
383
  if unused_classes:
373
384
  logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
@@ -375,9 +386,8 @@ def main() -> None:
375
386
  for i, item in enumerate(unused_classes, 1):
376
387
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
377
388
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
378
-
379
389
  else:
380
- logger.info(f"\n{Colors.GREEN}✓ All variables are being used!{Colors.RESET}")
390
+ logger.info(f"\n{Colors.GREEN}✓ All classes are being used!{Colors.RESET}")
381
391
 
382
392
  dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
383
393
 
@@ -1,5 +1,4 @@
1
1
  import re
2
- from pathlib import Path
3
2
 
4
3
  PENALTIES = {
5
4
  "private_name": 80,
@@ -35,10 +34,10 @@ DEFAULT_EXCLUDE_FOLDERS = {
35
34
  "htmlcov", ".coverage", "build", "dist", "*.egg-info", "venv", ".venv"
36
35
  }
37
36
 
38
- def is_test_path(p: Path | str) -> bool:
37
+ def is_test_path(p):
39
38
  return bool(TEST_FILE_RE.search(str(p)))
40
39
 
41
- def is_framework_path(p: Path | str) -> bool:
40
+ def is_framework_path(p):
42
41
  return bool(FRAMEWORK_FILE_RE.search(str(p)))
43
42
 
44
43
  def parse_exclude_folders(user_exclude_folders= None, use_defaults= True, include_folders= None):
@@ -3,13 +3,15 @@ import ast
3
3
  from pathlib import Path
4
4
  import re
5
5
 
6
- PYTHON_BUILTINS={"print", "len", "str", "int", "float", "list", "dict", "set", "tuple", "range", "open",
6
+ PYTHON_BUILTINS={"print", "len", "str", "int", "float", "list", "dict", "set", "tuple", "range", "open", "reversed",
7
7
  "super", "object", "type", "enumerate", "zip", "map", "filter", "sorted", "sum", "min",
8
8
  "next", "iter", "bytes", "bytearray", "format", "round", "abs", "complex", "hash", "id", "bool", "callable",
9
9
  "getattr", "max", "all", "any", "setattr", "hasattr", "isinstance", "globals", "locals",
10
10
  "vars", "dir" ,"property", "classmethod", "staticmethod"}
11
11
  DYNAMIC_PATTERNS={"getattr", "globals", "eval", "exec"}
12
12
 
13
+ ## "🥚" hi :)
14
+
13
15
  class Definition:
14
16
 
15
17
  def __init__(self, name, t, filename, line):
@@ -60,9 +62,17 @@ class Visitor(ast.NodeVisitor):
60
62
  self.local_var_maps = []
61
63
  self.in_cst_class = 0
62
64
  self.local_type_maps = []
65
+ self._dataclass_stack = []
66
+ self.dataclass_fields = set()
67
+ self.first_read_lineno = {}
63
68
 
64
69
  def add_def(self, name, t, line):
65
- if name not in {d.name for d in self.defs}:
70
+ found = False
71
+ for d in self.defs:
72
+ if d.name == name:
73
+ found = True
74
+ break
75
+ if not found:
66
76
  self.defs.append(Definition(name, t, self.file, line))
67
77
 
68
78
  def add_ref(self, name):
@@ -72,8 +82,23 @@ class Visitor(ast.NodeVisitor):
72
82
  if name in self.alias:
73
83
  return self.alias[name]
74
84
  if name in PYTHON_BUILTINS:
85
+ if self.mod:
86
+ mod_candidate = f"{self.mod}.{name}"
87
+ else:
88
+ mod_candidate = name
89
+ if any(d.name == mod_candidate for d in self.defs):
90
+ return mod_candidate
91
+
92
+ if self.mod:
93
+ return f"{self.mod}.{name}"
94
+ else:
75
95
  return name
76
- return f"{self.mod}.{name}" if self.mod else name
96
+
97
+ def visit_Global(self, node):
98
+ if self.current_function_scope and self.local_var_maps:
99
+ for name in node.names:
100
+ self.local_var_maps[-1][name] = f"{self.mod}.{name}"
101
+ return
77
102
 
78
103
  def visit_annotation(self, node):
79
104
  if node is not None:
@@ -148,7 +173,11 @@ class Visitor(ast.NodeVisitor):
148
173
 
149
174
  qualified_name= ".".join(filter(None, name_parts))
150
175
 
151
- self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
176
+ if self.cls:
177
+ def_type = "method"
178
+ else:
179
+ def_type = "function"
180
+ self.add_def(qualified_name, def_type, node.lineno)
152
181
 
153
182
  self.current_function_scope.append(node.name)
154
183
  self.local_var_maps.append({})
@@ -183,19 +212,41 @@ class Visitor(ast.NodeVisitor):
183
212
  self.add_def(cname, "class",node.lineno)
184
213
 
185
214
  is_cst = False
186
-
215
+ is_dc = False
216
+
187
217
  for base in node.bases:
188
218
  base_name = ""
219
+
189
220
  if isinstance(base, ast.Attribute):
190
221
  base_name = base.attr
222
+
191
223
  elif isinstance(base, ast.Name):
192
224
  base_name = base.id
193
225
  self.visit(base)
226
+
194
227
  if base_name in {"CSTTransformer", "CSTVisitor"}:
195
228
  is_cst = True
229
+
196
230
  for keyword in node.keywords:
197
231
  self.visit(keyword.value)
232
+
198
233
  for decorator in node.decorator_list:
234
+ def _is_dc(dec):
235
+ if isinstance(dec, ast.Call):
236
+ target = dec.func
237
+ else:
238
+ target = dec
239
+
240
+ if isinstance(target, ast.Name):
241
+ return target.id == "dataclass"
242
+
243
+ if isinstance(target, ast.Attribute):
244
+ return target.attr == "dataclass"
245
+
246
+ return False
247
+
248
+ if _is_dc(decorator):
249
+ is_dc = True
199
250
  self.visit(decorator)
200
251
 
201
252
  prev= self.cls
@@ -203,10 +254,13 @@ class Visitor(ast.NodeVisitor):
203
254
  self.in_cst_class += 1
204
255
 
205
256
  self.cls= node.name
257
+ self._dataclass_stack.append(is_dc)
206
258
  for b in node.body:
207
259
  self.visit(b)
208
260
 
209
261
  self.cls= prev
262
+ self._dataclass_stack.pop()
263
+
210
264
  if is_cst:
211
265
  self.in_cst_class -= 1
212
266
 
@@ -220,6 +274,7 @@ class Visitor(ast.NodeVisitor):
220
274
  name_simple = t.id
221
275
  scope_parts = [self.mod]
222
276
  if self.cls: scope_parts.append(self.cls)
277
+
223
278
  if self.current_function_scope: scope_parts.extend(self.current_function_scope)
224
279
  prefix = '.'.join(filter(None, scope_parts))
225
280
  if prefix:
@@ -228,9 +283,16 @@ class Visitor(ast.NodeVisitor):
228
283
  var_name = name_simple
229
284
 
230
285
  self.add_def(var_name, "variable", t.lineno)
286
+ if (self._dataclass_stack and self._dataclass_stack[-1]
287
+ and self.cls
288
+ and not self.current_function_scope):
289
+ self.dataclass_fields.add(var_name)
290
+
231
291
  if self.current_function_scope and self.local_var_maps:
232
292
  self.local_var_maps[-1][name_simple] = var_name
293
+
233
294
  elif isinstance(t, (ast.Tuple, ast.List)):
295
+
234
296
  for elt in t.elts:
235
297
  _define(elt)
236
298
  _define(node.target)
@@ -243,19 +305,24 @@ class Visitor(ast.NodeVisitor):
243
305
  self.local_var_maps and
244
306
  nm in self.local_var_maps[-1]):
245
307
 
246
- self.add_ref(self.local_var_maps[-1][nm])
308
+ # self.add_ref(self.local_var_maps[-1][nm])
309
+ fq = self.local_var_maps[-1][nm]
310
+ self.add_ref(fq)
311
+ var_name = fq
247
312
 
248
313
  else:
249
314
  self.add_ref(self.qual(nm))
250
- scope_parts = [self.mod]
251
- if self.cls: scope_parts.append(self.cls)
315
+ scope_parts = [self.mod]
316
+ if self.cls:
317
+ scope_parts.append(self.cls)
252
318
 
253
- if self.current_function_scope: scope_parts.extend(self.current_function_scope)
254
- prefix = '.'.join(filter(None, scope_parts))
255
- if prefix:
256
- var_name = f"{prefix}.{nm}"
257
- else:
258
- var_name = nm
319
+ if self.current_function_scope:
320
+ scope_parts.extend(self.current_function_scope)
321
+ prefix = '.'.join(filter(None, scope_parts))
322
+ if prefix:
323
+ var_name = f"{prefix}.{nm}"
324
+ else:
325
+ var_name = nm
259
326
 
260
327
  self.add_def(var_name, "variable", node.lineno)
261
328
  if self.current_function_scope and self.local_var_maps:
@@ -292,14 +359,19 @@ class Visitor(ast.NodeVisitor):
292
359
  if self.current_function_scope:
293
360
  scope_parts.extend(self.current_function_scope)
294
361
 
295
- prefix = '.'.join(filter(None, scope_parts))
296
- if prefix:
297
- var_name = f"{prefix}.{name_simple}"
362
+ if (self.current_function_scope and self.local_var_maps
363
+ and name_simple in self.local_var_maps[-1]):
364
+ var_name = self.local_var_maps[-1][name_simple]
298
365
  else:
299
- var_name = name_simple
366
+ prefix = '.'.join(filter(None, scope_parts))
367
+ if prefix:
368
+ var_name = f"{prefix}.{name_simple}"
369
+ else:
370
+ var_name = name_simple
300
371
 
301
372
  self.add_def(var_name, "variable", target_node.lineno)
302
- if self.current_function_scope and self.local_var_maps:
373
+ if (self.current_function_scope and self.local_var_maps
374
+ and name_simple not in self.local_var_maps[-1]):
303
375
  self.local_var_maps[-1][name_simple] = var_name
304
376
 
305
377
  elif isinstance(target_node, (ast.Tuple, ast.List)):
@@ -403,18 +475,19 @@ class Visitor(ast.NodeVisitor):
403
475
  if isinstance(node.ctx, ast.Load):
404
476
  for param_name, param_full_name in self.current_function_params:
405
477
  if node.id == param_name:
478
+ self.first_read_lineno.setdefault(param_full_name, node.lineno)
406
479
  self.add_ref(param_full_name)
407
480
  return
408
481
 
409
- if (self.current_function_scope and
410
- self.local_var_maps
411
- and self.local_var_maps
482
+ if (self.current_function_scope and self.local_var_maps
412
483
  and node.id in self.local_var_maps[-1]):
413
-
414
- self.add_ref(self.local_var_maps[-1][node.id])
484
+ fq = self.local_var_maps[-1][node.id]
485
+ self.first_read_lineno.setdefault(fq, node.lineno)
486
+ self.add_ref(fq)
415
487
  return
416
488
 
417
489
  qualified = self.qual(node.id)
490
+ self.first_read_lineno.setdefault(qualified, node.lineno)
418
491
  self.add_ref(qualified)
419
492
  if node.id in DYNAMIC_PATTERNS:
420
493
  self.dyn.add(self.mod.split(".")[0])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -27,6 +27,7 @@ test/test_codemods.py
27
27
  test/test_constants.py
28
28
  test/test_framework_aware.py
29
29
  test/test_integration.py
30
+ test/test_new_behaviours.py
30
31
  test/test_skylos.py
31
32
  test/test_test_aware.py
32
33
  test/test_visitor.py
@@ -8,11 +8,9 @@ from skylos.test_aware import TestAwareVisitor
8
8
  from skylos.framework_aware import FrameworkAwareVisitor
9
9
 
10
10
  from skylos.analyzer import (
11
- Skylos,
12
- parse_exclude_folders,
11
+ Skylos,
13
12
  proc_file,
14
- analyze,
15
- DEFAULT_EXCLUDE_FOLDERS,
13
+ analyze
16
14
  )
17
15
 
18
16
  @pytest.fixture
@@ -97,40 +95,6 @@ def internal_function():
97
95
 
98
96
  yield temp_path
99
97
 
100
- class TestParseExcludeFolders:
101
-
102
- def test_default_exclude_folders_included(self):
103
- """default folders are included by default."""
104
- result = parse_exclude_folders(None, use_defaults=True)
105
- assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
106
-
107
- def test_default_exclude_folders_disabled(self):
108
- """default folders can be disabled."""
109
- result = parse_exclude_folders(None, use_defaults=False)
110
- assert not DEFAULT_EXCLUDE_FOLDERS.intersection(result)
111
-
112
- def test_user_exclude_folders_added(self):
113
- """user-specified folders are added."""
114
- user_folders = {"custom_folder", "another_folder"}
115
- result = parse_exclude_folders(user_folders, use_defaults=True)
116
- assert user_folders.issubset(result)
117
- assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
118
-
119
- def test_include_folders_override_defaults(self):
120
- """include_folders can override defaults."""
121
- include_folders = {"__pycache__", ".git"}
122
- result = parse_exclude_folders(None, use_defaults=True, include_folders=include_folders)
123
- for folder in include_folders:
124
- assert folder not in result
125
-
126
- def test_include_folders_override_user_excludes(self):
127
- """include_folders can override user excludes."""
128
- user_excludes = {"custom_folder", "another_folder"}
129
- include_folders = {"custom_folder"}
130
- result = parse_exclude_folders(user_excludes, use_defaults=False, include_folders=include_folders)
131
- assert "custom_folder" not in result
132
- assert "another_folder" in result
133
-
134
98
  class TestSkylos:
135
99
 
136
100
  @pytest.fixture
@@ -144,7 +108,6 @@ class TestSkylos:
144
108
  assert isinstance(skylos.exports, defaultdict)
145
109
 
146
110
  def test_module_name_generation(self, skylos):
147
- """Test module name generation from file paths."""
148
111
  root = Path("/project")
149
112
 
150
113
  # test a regular Python file
@@ -315,7 +278,7 @@ class TestHeuristics:
315
278
 
316
279
  def test_auto_called_methods_get_references(self, skylos_with_class_methods):
317
280
  """auto-called methods get reference counts when class is used."""
318
- skylos, mock_class, mock_init, mock_enter = skylos_with_class_methods
281
+ skylos, _, mock_init, mock_enter = skylos_with_class_methods
319
282
 
320
283
  skylos._apply_heuristics()
321
284
 
@@ -451,7 +414,7 @@ class TestClass:
451
414
  mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
452
415
  mock_framework_visitor_class.return_value = mock_framework_visitor
453
416
 
454
- defs, refs, dyn, exports, test_flags, framework_flags, _ = proc_file(f.name, "test_module")
417
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
455
418
 
456
419
  mock_visitor_class.assert_called_once_with("test_module", f.name)
457
420
  mock_visitor.visit.assert_called_once()
@@ -472,7 +435,7 @@ class TestClass:
472
435
  f.flush()
473
436
 
474
437
  try:
475
- defs, refs, dyn, exports, test_flags, framework_flags, _ = proc_file(f.name, "test_module")
438
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
476
439
 
477
440
  assert defs == []
478
441
  assert refs == []
@@ -506,7 +469,7 @@ class TestClass:
506
469
  mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
507
470
  mock_framework_visitor_class.return_value = mock_framework_visitor
508
471
 
509
- defs, refs, dyn, exports, test_flags, framework_flags, _ = proc_file((f.name, "test_module"))
472
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file((f.name, "test_module"))
510
473
 
511
474
  mock_visitor_class.assert_called_once_with("test_module", f.name)
512
475
  finally:
@@ -0,0 +1,52 @@
1
+ import json
2
+ from skylos.analyzer import analyze
3
+
4
+ def test_dataclass_fields(tmp_path):
5
+ p = tmp_path / "dc.py"
6
+ p.write_text(
7
+ "from dataclasses import dataclass\n"
8
+ "@dataclass\n"
9
+ "class C:\n"
10
+ " x: int = 1\n"
11
+ " y: int = 2\n"
12
+ "def f(c: C):\n"
13
+ " return c.x + c.y\n"
14
+ "f(C())\n"
15
+ )
16
+ result = json.loads(analyze(str(tmp_path), conf=0))
17
+ assert result['unused_variables'] == []
18
+
19
+ def test_dead_store_guard(tmp_path):
20
+ p = tmp_path / "dstore.py"
21
+ p.write_text(
22
+ "lst=[1,2,3]\n"
23
+ "dir=0\n"
24
+ "if 'a' in lst:\n"
25
+ " pass\n"
26
+ "else:\n"
27
+ " dir=1\n"
28
+ "print(dir)\n"
29
+ )
30
+ result = json.loads(analyze(str(tmp_path), conf=0))
31
+ assert result['unused_variables'] == []
32
+
33
+ def test_unused_constant_reported(tmp_path):
34
+ p = tmp_path / "pool.py"
35
+ p.write_text(
36
+ "from concurrent.futures import ProcessPoolExecutor\n"
37
+ "PROCESS_POOL=None\n"
38
+ "NEVER_USED=123\n"
39
+ "def get_pool():\n"
40
+ " global PROCESS_POOL\n"
41
+ " if PROCESS_POOL is None:\n"
42
+ " PROCESS_POOL=ProcessPoolExecutor(max_workers=2)\n"
43
+ " return PROCESS_POOL\n"
44
+ "get_pool()\n"
45
+ )
46
+ result = json.loads(analyze(str(tmp_path), conf=0))
47
+ names = set()
48
+ for v in result['unused_variables']:
49
+ names.add(v['name'])
50
+
51
+ assert 'NEVER_USED' in names
52
+ assert 'PROCESS_POOL' not in names
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