skylos 1.1.11__py3-none-any.whl → 1.2.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.

test/test_analyzer.py CHANGED
@@ -1,36 +1,101 @@
1
- #!/usr/bin/env python3
2
1
  import pytest
3
2
  import json
4
3
  import tempfile
5
4
  from pathlib import Path
6
5
  from unittest.mock import Mock, patch
7
6
  from collections import defaultdict
7
+ from skylos.test_aware import TestAwareVisitor
8
+ from skylos.framework_aware import FrameworkAwareVisitor
8
9
 
9
- try:
10
- from skylos.analyzer import (
11
- Skylos,
12
- parse_exclude_folders,
13
- proc_file,
14
- analyze,
15
- DEFAULT_EXCLUDE_FOLDERS,
16
- AUTO_CALLED,
17
- MAGIC_METHODS,
18
- TEST_METHOD_PATTERN
19
- )
20
- except ImportError:
21
- import sys
22
- from pathlib import Path
23
- sys.path.insert(0, str(Path(__file__).parent.parent))
24
- from skylos.analyzer import (
25
- Skylos,
26
- parse_exclude_folders,
27
- proc_file,
28
- analyze,
29
- DEFAULT_EXCLUDE_FOLDERS,
30
- AUTO_CALLED,
31
- MAGIC_METHODS,
32
- TEST_METHOD_PATTERN
33
- )
10
+ from skylos.analyzer import (
11
+ Skylos,
12
+ parse_exclude_folders,
13
+ proc_file,
14
+ analyze,
15
+ DEFAULT_EXCLUDE_FOLDERS,
16
+ )
17
+
18
+ @pytest.fixture
19
+ def mock_definition():
20
+ def _create_mock_def(name, simple_name, type, references=0, is_exported=False, confidence=100, in_init=False, line=1):
21
+ mock = Mock()
22
+ mock.name = name
23
+ mock.simple_name = simple_name
24
+ mock.type = type
25
+ mock.references = references
26
+ mock.is_exported = is_exported
27
+ mock.confidence = confidence
28
+ mock.in_init = in_init
29
+ mock.line = line
30
+ mock.to_dict.return_value = {
31
+ "name": name,
32
+ "type": type,
33
+ "file": "test.py",
34
+ "line": line
35
+ }
36
+ return mock
37
+ return _create_mock_def
38
+
39
+ @pytest.fixture
40
+ def mock_test_aware_visitor():
41
+ mock = Mock(spec=TestAwareVisitor)
42
+ mock.is_test_file = False
43
+ mock.test_decorated_lines = set()
44
+ return mock
45
+
46
+ @pytest.fixture
47
+ def mock_framework_aware_visitor():
48
+ mock = Mock(spec=FrameworkAwareVisitor)
49
+ mock.framework_decorated_lines = set()
50
+ return mock
51
+
52
+ @pytest.fixture
53
+ def temp_python_project():
54
+ """Create a temp Python project for testing"""
55
+ with tempfile.TemporaryDirectory() as temp_dir:
56
+ temp_path = Path(temp_dir)
57
+
58
+ main_py = temp_path / "main.py"
59
+ main_py.write_text("""
60
+ def used_function():
61
+ return "used"
62
+
63
+ def unused_function():
64
+ return "unused"
65
+
66
+ class UsedClass:
67
+ def method(self):
68
+ pass
69
+
70
+ class UnusedClass:
71
+ def method(self):
72
+ pass
73
+
74
+ result = used_function()
75
+ instance = UsedClass()
76
+ """)
77
+
78
+ package_dir = temp_path / "mypackage"
79
+ package_dir.mkdir()
80
+
81
+ init_py = package_dir / "__init__.py"
82
+ init_py.write_text("""
83
+ from .module import exported_function
84
+
85
+ def internal_function():
86
+ pass
87
+ """)
88
+
89
+ module_py = package_dir / "module.py"
90
+ module_py.write_text("""
91
+ def exported_function():
92
+ return "exported"
93
+
94
+ def internal_function():
95
+ return "internal"
96
+ """)
97
+
98
+ yield temp_path
34
99
 
35
100
  class TestParseExcludeFolders:
36
101
 
@@ -66,7 +131,6 @@ class TestParseExcludeFolders:
66
131
  assert "custom_folder" not in result
67
132
  assert "another_folder" in result
68
133
 
69
-
70
134
  class TestSkylos:
71
135
 
72
136
  @pytest.fixture
@@ -214,7 +278,6 @@ class TestSkylos:
214
278
  assert mock_import.references == 1
215
279
  assert mock_original.references == 1
216
280
 
217
-
218
281
  class TestHeuristics:
219
282
 
220
283
  @pytest.fixture
@@ -258,136 +321,9 @@ class TestHeuristics:
258
321
 
259
322
  assert mock_init.references == 1
260
323
  assert mock_enter.references == 1
261
-
262
- def test_magic_methods_confidence_zero(self, mock_definition):
263
- """magic methods get confidence of 0."""
264
- skylos = Skylos()
265
-
266
- mock_magic = mock_definition(
267
- name="MyClass.__str__",
268
- simple_name="__str__",
269
- type="method",
270
- confidence=100
271
- )
272
-
273
- skylos.defs = {"MyClass.__str__": mock_magic}
274
- skylos._apply_heuristics()
275
-
276
- assert mock_magic.confidence == 0
277
-
278
- def test_self_cls_parameters_confidence_zero(self, mock_definition):
279
- """self/cls parameters get confidence of 0"""
280
- skylos = Skylos()
281
-
282
- mock_self = mock_definition(
283
- name="self",
284
- simple_name="self",
285
- type="parameter",
286
- confidence=100
287
- )
288
-
289
- mock_cls = mock_definition(
290
- name="cls",
291
- simple_name="cls",
292
- type="parameter",
293
- confidence=100
294
- )
295
-
296
- skylos.defs = {"self": mock_self, "cls": mock_cls}
297
- skylos._apply_heuristics()
298
-
299
- assert mock_self.confidence == 0
300
- assert mock_cls.confidence == 0
301
-
302
- def test_test_methods_confidence_zero(self, mock_definition):
303
- """test methods in test classes get confidence of 0"""
304
- skylos = Skylos()
305
-
306
- mock_test_method = mock_definition(
307
- name="TestMyClass.test_something",
308
- simple_name="test_something",
309
- type="method",
310
- confidence=100
311
- )
312
-
313
- skylos.defs = {"TestMyClass.test_something": mock_test_method}
314
- skylos._apply_heuristics()
315
-
316
- assert mock_test_method.confidence == 0
317
-
318
- def test_underscore_variable_confidence_zero(self, mock_definition):
319
- """underscore variables get confidence of 0."""
320
- skylos = Skylos()
321
-
322
- mock_underscore = mock_definition(
323
- name="_",
324
- simple_name="_",
325
- type="variable",
326
- confidence=100
327
- )
328
-
329
- skylos.defs = {"_": mock_underscore}
330
- skylos._apply_heuristics()
331
-
332
- assert mock_underscore.confidence == 0
333
-
334
324
 
335
325
  class TestAnalyze:
336
326
 
337
- @pytest.fixture
338
- def temp_python_project(self):
339
- """Create a temp Python project for testing"""
340
- with tempfile.TemporaryDirectory() as temp_dir:
341
- temp_path = Path(temp_dir)
342
-
343
- main_py = temp_path / "main.py"
344
- main_py.write_text("""
345
- def used_function():
346
- return "used"
347
-
348
- def unused_function():
349
- return "unused"
350
-
351
- class UsedClass:
352
- def method(self):
353
- pass
354
-
355
- class UnusedClass:
356
- def method(self):
357
- pass
358
-
359
- result = used_function()
360
- instance = UsedClass()
361
- """)
362
-
363
- package_dir = temp_path / "mypackage"
364
- package_dir.mkdir()
365
-
366
- init_py = package_dir / "__init__.py"
367
- init_py.write_text("""
368
- from .module import exported_function
369
-
370
- def internal_function():
371
- pass
372
- """)
373
-
374
- module_py = package_dir / "module.py"
375
- module_py.write_text("""
376
- def exported_function():
377
- return "exported"
378
-
379
- def internal_function():
380
- return "internal"
381
- """)
382
-
383
- test_dir = temp_path / "__pycache__"
384
- test_dir.mkdir()
385
-
386
- test_file = test_dir / "cached.pyc"
387
- test_file.write_text("# This should be excluded")
388
-
389
- yield temp_path
390
-
391
327
  @patch('skylos.analyzer.proc_file')
392
328
  def test_analyze_basic(self, mock_proc_file, temp_python_project):
393
329
  mock_def = Mock()
@@ -398,18 +334,26 @@ def internal_function():
398
334
  mock_def.type = "function"
399
335
  mock_def.to_dict.return_value = {
400
336
  "name": "test.unused_function",
401
- "type": "function",
337
+ "type": "function",
402
338
  "file": "test.py",
403
339
  "line": 1
404
340
  }
405
341
 
406
- mock_proc_file.return_value = ([mock_def], [], set(), set())
342
+ mock_test_visitor = Mock(spec=TestAwareVisitor)
343
+ mock_test_visitor.is_test_file = False
344
+ mock_test_visitor.test_decorated_lines = set()
345
+
346
+ mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
347
+ mock_framework_visitor.framework_decorated_lines = set()
348
+ mock_framework_visitor.is_framework_file = False
349
+
350
+ mock_proc_file.return_value = ([mock_def], [], set(), set(), mock_test_visitor, mock_framework_visitor)
407
351
 
408
352
  result_json = analyze(str(temp_python_project), conf=60)
409
353
  result = json.loads(result_json)
410
354
 
411
355
  assert "unused_functions" in result
412
- assert "unused_imports" in result
356
+ assert "unused_imports" in result
413
357
  assert "unused_classes" in result
414
358
  assert "unused_variables" in result
415
359
  assert "unused_parameters" in result
@@ -422,7 +366,7 @@ def internal_function():
422
366
  exclude_file = exclude_dir / "generated.py"
423
367
  exclude_file.write_text("def generated_function(): pass")
424
368
 
425
- result_json = analyze(str(temp_python_project), exclude_folders=["build"]) # Use list instead of set
369
+ result_json = analyze(str(temp_python_project), exclude_folders=["build"])
426
370
  result = json.loads(result_json)
427
371
 
428
372
  assert result["analysis_summary"]["excluded_folders"] == ["build"]
@@ -466,7 +410,7 @@ def internal_function():
466
410
  mock_get_files.return_value = ([Path("/fake/file.py")], Path("/"))
467
411
 
468
412
  with patch('skylos.analyzer.proc_file') as mock_proc_file:
469
- mock_proc_file.return_value = ([], [], set(), set())
413
+ mock_proc_file.return_value = ([], [], set(), set(), Mock(spec=TestAwareVisitor), Mock(spec=FrameworkAwareVisitor))
470
414
 
471
415
  result_json = skylos.analyze("/fake/path", thr=60)
472
416
  result = json.loads(result_json)
@@ -475,7 +419,6 @@ def internal_function():
475
419
  assert len(result["unused_functions"]) == 1
476
420
  assert result["unused_functions"][0]["name"] == "high_conf"
477
421
 
478
-
479
422
  class TestProcFile:
480
423
 
481
424
  def test_proc_file_with_valid_python(self):
@@ -491,7 +434,10 @@ class TestClass:
491
434
  f.flush()
492
435
 
493
436
  try:
494
- with patch('skylos.analyzer.Visitor') as mock_visitor_class:
437
+ with patch('skylos.analyzer.Visitor') as mock_visitor_class, \
438
+ patch('skylos.analyzer.TestAwareVisitor') as mock_test_visitor_class, \
439
+ patch('skylos.analyzer.FrameworkAwareVisitor') as mock_framework_visitor_class:
440
+
495
441
  mock_visitor = Mock()
496
442
  mock_visitor.defs = []
497
443
  mock_visitor.refs = []
@@ -499,7 +445,13 @@ class TestClass:
499
445
  mock_visitor.exports = set()
500
446
  mock_visitor_class.return_value = mock_visitor
501
447
 
502
- defs, refs, dyn, exports = proc_file(f.name, "test_module")
448
+ mock_test_visitor = Mock(spec=TestAwareVisitor)
449
+ mock_test_visitor_class.return_value = mock_test_visitor
450
+
451
+ mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
452
+ mock_framework_visitor_class.return_value = mock_framework_visitor
453
+
454
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
503
455
 
504
456
  mock_visitor_class.assert_called_once_with("test_module", f.name)
505
457
  mock_visitor.visit.assert_called_once()
@@ -507,7 +459,10 @@ class TestClass:
507
459
  assert defs == []
508
460
  assert refs == []
509
461
  assert dyn == set()
462
+ ## added new test here for the new methods
510
463
  assert exports == set()
464
+ assert test_flags == mock_test_visitor
465
+ assert framework_flags == mock_framework_visitor
511
466
  finally:
512
467
  Path(f.name).unlink()
513
468
 
@@ -517,12 +472,14 @@ class TestClass:
517
472
  f.flush()
518
473
 
519
474
  try:
520
- defs, refs, dyn, exports = proc_file(f.name, "test_module")
475
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
521
476
 
522
477
  assert defs == []
523
478
  assert refs == []
524
479
  assert dyn == set()
525
480
  assert exports == set()
481
+ assert isinstance(test_flags, TestAwareVisitor)
482
+ assert isinstance(framework_flags, FrameworkAwareVisitor)
526
483
  finally:
527
484
  Path(f.name).unlink()
528
485
 
@@ -532,7 +489,10 @@ class TestClass:
532
489
  f.flush()
533
490
 
534
491
  try:
535
- with patch('skylos.analyzer.Visitor') as mock_visitor_class:
492
+ with patch('skylos.analyzer.Visitor') as mock_visitor_class, \
493
+ patch('skylos.analyzer.TestAwareVisitor') as mock_test_visitor_class, \
494
+ patch('skylos.analyzer.FrameworkAwareVisitor') as mock_framework_visitor_class:
495
+
536
496
  mock_visitor = Mock()
537
497
  mock_visitor.defs = []
538
498
  mock_visitor.refs = []
@@ -540,45 +500,113 @@ class TestClass:
540
500
  mock_visitor.exports = set()
541
501
  mock_visitor_class.return_value = mock_visitor
542
502
 
543
- defs, refs, dyn, exports = proc_file((f.name, "test_module"))
503
+ mock_test_visitor = Mock(spec=TestAwareVisitor)
504
+ mock_test_visitor_class.return_value = mock_test_visitor
505
+
506
+ mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
507
+ mock_framework_visitor_class.return_value = mock_framework_visitor
508
+
509
+ # defs, refs, dyn, exports, test_flags, framework_flags = proc_file((f.name, "test_module"))
544
510
 
545
511
  mock_visitor_class.assert_called_once_with("test_module", f.name)
546
512
  finally:
547
513
  Path(f.name).unlink()
548
514
 
549
-
550
- class TestConstants:
515
+ class TestApplyPenalties:
551
516
 
552
- def test_auto_called_contains_expected_methods(self):
553
- """ AUTO_CALLED contains expected magic methods."""
554
- assert "__init__" in AUTO_CALLED
555
- assert "__enter__" in AUTO_CALLED
556
- assert "__exit__" in AUTO_CALLED
517
+ @patch('skylos.analyzer.detect_framework_usage')
518
+ def test_private_name_penalty(self, mock_detect_framework, mock_definition, mock_test_aware_visitor, mock_framework_aware_visitor):
519
+ """private names get penalized."""
520
+ mock_detect_framework.return_value = None # or whatever confidence value here that can change later on
521
+
522
+ skylos = Skylos()
523
+ mock_def = mock_definition(
524
+ name="_private_func",
525
+ simple_name="_private_func",
526
+ type="function",
527
+ confidence=100
528
+ )
529
+
530
+ skylos._apply_penalties(mock_def, mock_test_aware_visitor, mock_framework_aware_visitor)
531
+ assert mock_def.confidence < 100
532
+
533
+ @patch('skylos.analyzer.detect_framework_usage')
534
+ def test_magic_methods_confidence_zero(self, mock_detect_framework, mock_definition, mock_test_aware_visitor, mock_framework_aware_visitor):
535
+ """magic methods get confidence of 0."""
536
+ mock_detect_framework.return_value = None
537
+ skylos = Skylos()
538
+ mock_def = mock_definition(
539
+ name="MyClass.__str__",
540
+ simple_name="__str__",
541
+ type="method",
542
+ confidence=100
543
+ )
544
+
545
+ skylos._apply_penalties(mock_def, mock_test_aware_visitor, mock_framework_aware_visitor)
546
+ assert mock_def.confidence == 0
557
547
 
558
- def test_magic_methods_contains_common_methods(self):
559
- """ MAGIC_METHODS contains common magic methods."""
560
- assert "__str__" in MAGIC_METHODS
561
- assert "__repr__" in MAGIC_METHODS
562
- assert "__eq__" in MAGIC_METHODS
563
- assert "__len__" in MAGIC_METHODS
548
+ @patch('skylos.analyzer.detect_framework_usage')
549
+ def test_self_cls_parameters_confidence_zero(self, mock_detect_framework, mock_definition, mock_test_aware_visitor, mock_framework_aware_visitor):
550
+ mock_detect_framework.return_value = None
551
+ skylos = Skylos()
552
+
553
+ mock_self = mock_definition(
554
+ name="MyClass.method.self",
555
+ simple_name="self",
556
+ type="parameter",
557
+ confidence=100
558
+ )
559
+
560
+ mock_cls = mock_definition(
561
+ name="MyClass.classmethod.cls",
562
+ simple_name="cls",
563
+ type="parameter",
564
+ confidence=100
565
+ )
566
+
567
+ skylos._apply_penalties(mock_self, mock_test_aware_visitor, mock_framework_aware_visitor)
568
+ skylos._apply_penalties(mock_cls, mock_test_aware_visitor, mock_framework_aware_visitor)
569
+
570
+ assert mock_self.confidence == 0
571
+ assert mock_cls.confidence == 0
564
572
 
565
- def test_test_method_pattern_matches_correctly(self):
566
- """ TEST_METHOD_PATTERN matches test methods correctly."""
567
- assert TEST_METHOD_PATTERN.match("test_something")
568
- assert TEST_METHOD_PATTERN.match("test_another_thing")
569
- assert TEST_METHOD_PATTERN.match("test_123")
570
- assert not TEST_METHOD_PATTERN.match("not_a_test")
571
- assert not TEST_METHOD_PATTERN.match("test") # no underscore
572
- assert not TEST_METHOD_PATTERN.match("testing_something") # doesnt start with test_
573
+ @patch('skylos.analyzer.detect_framework_usage')
574
+ def test_test_methods_confidence_zero(self, mock_detect_framework, mock_definition, mock_framework_aware_visitor):
575
+ """test methods get confidence of 0"""
576
+ mock_detect_framework.return_value = None
577
+
578
+ skylos = Skylos()
579
+
580
+ test_visitor = Mock(spec=TestAwareVisitor)
581
+ test_visitor.is_test_file = True
582
+ test_visitor.test_decorated_lines = set()
583
+
584
+ mock_def = mock_definition(
585
+ name="TestMyClass.test_something",
586
+ simple_name="test_something",
587
+ type="method",
588
+ confidence=100
589
+ )
590
+
591
+ skylos._apply_penalties(mock_def, test_visitor, mock_framework_aware_visitor)
592
+ assert mock_def.confidence == 0
573
593
 
574
- def test_default_exclude_folders_contains_expected(self):
575
- """ DEFAULT_EXCLUDE_FOLDERS contains expected directories."""
576
- expected_folders = {
577
- "__pycache__", ".git", ".pytest_cache", ".mypy_cache",
578
- ".tox", "htmlcov", ".coverage", "build", "dist",
579
- "*.egg-info", "venv", ".venv"
580
- }
581
- assert expected_folders.issubset(DEFAULT_EXCLUDE_FOLDERS)
594
+ @patch('skylos.analyzer.detect_framework_usage')
595
+ def test_underscore_variable_confidence_zero(self, mock_detect_framework, mock_definition, mock_test_aware_visitor, mock_framework_aware_visitor):
596
+ """underscore variables get confidence of 0."""
597
+ mock_detect_framework.return_value = None
598
+
599
+ skylos = Skylos()
600
+
601
+ mock_def = mock_definition(
602
+ name="_",
603
+ simple_name="_",
604
+ type="variable",
605
+ confidence=100
606
+ )
607
+
608
+ skylos._apply_penalties(mock_def, mock_test_aware_visitor, mock_framework_aware_visitor)
609
+ assert mock_def.confidence == 0
582
610
 
583
611
  if __name__ == "__main__":
584
612
  pytest.main([__file__, "-v"])
@@ -0,0 +1,149 @@
1
+ import pytest
2
+ import tempfile
3
+ import json
4
+ from pathlib import Path
5
+ from skylos.analyzer import Skylos
6
+
7
+ class TestChangeAnalyzer:
8
+
9
+ def test_future_annotations_not_flagged(self):
10
+ """from __future__ import annotations should not be flagged as unused"""
11
+ code = '''
12
+ from __future__ import annotations
13
+ import ast # should be flagged
14
+
15
+ def func(x: int) -> int:
16
+ return x * 2
17
+ '''
18
+ result = self._analyze(code)
19
+ import_names = [i["simple_name"] for i in result["unused_imports"]]
20
+
21
+ assert "annotations" not in import_names, "__future__ annotations wrongly flagged"
22
+ assert "ast" in import_names, "Regular unused import should also be flagged"
23
+
24
+ def test_underscore_items_not_flagged(self):
25
+ """items starting with _ should not be reported as unused"""
26
+ code = '''
27
+ _private_var = "private"
28
+
29
+ def _private_func():
30
+ return "private"
31
+
32
+ def regular_func(_private_param):
33
+ return "test"
34
+
35
+ class Example:
36
+ def _private_method(self):
37
+ return "private"
38
+ '''
39
+ result = self._analyze(code)
40
+
41
+ function_names = [f["name"] for f in result["unused_functions"]]
42
+ underscore_funcs = [name for name in function_names if name.startswith("_")]
43
+ assert len(underscore_funcs) == 0, f"Underscore functions incorrectly flagged: {underscore_funcs}"
44
+
45
+ variable_names = [v["name"] for v in result["unused_variables"]]
46
+ underscore_vars = [name for name in variable_names if name.startswith("_")]
47
+ assert len(underscore_vars) == 0, f"Underscore variables incorrectly flagged: {underscore_vars}"
48
+
49
+ param_names = [p["name"] for p in result["unused_parameters"]]
50
+ underscore_params = [name for name in param_names if name.startswith("_")]
51
+ assert len(underscore_params) == 0, f"Underscore parameters incorrectly flagged: {underscore_params}"
52
+
53
+ def test_unittest_magic_methods_not_flagged(self):
54
+ """setUp, tearDown, setUpClass should not be flagged as unused"""
55
+ code = '''
56
+ import unittest
57
+
58
+ class TestCase(unittest.TestCase):
59
+ def setUp(self):
60
+ self.data = "test"
61
+
62
+ def tearDown(self):
63
+ pass
64
+
65
+ @classmethod
66
+ def setUpClass(cls):
67
+ pass
68
+
69
+ @classmethod
70
+ def tearDownClass(cls):
71
+ pass
72
+
73
+ def test_example(self):
74
+ pass
75
+
76
+ def setUpModule():
77
+ pass
78
+
79
+ def tearDownModule():
80
+ pass
81
+ '''
82
+ result = self._analyze(code, "test_magic.py")
83
+ function_names = [f["name"] for f in result["unused_functions"]]
84
+
85
+ magic_methods = ["setUp", "tearDown", "setUpClass", "tearDownClass", "setUpModule", "tearDownModule"]
86
+ flagged_magic = [method for method in magic_methods if method in function_names]
87
+
88
+ assert len(flagged_magic) == 0, f"unittest/pytest methods incorrectly flagged: {flagged_magic}"
89
+
90
+ def test_all_edge_cases_together(self):
91
+ code = '''
92
+ from __future__ import annotations
93
+ import unused_import
94
+
95
+ _private_var = "private"
96
+
97
+ class TestExample:
98
+ def setUp(self):
99
+ self._data = "test"
100
+
101
+ def tearDown(self):
102
+ pass
103
+
104
+ def test_something(self):
105
+ return self._data
106
+
107
+ def _helper_method(self):
108
+ return "helper"
109
+
110
+ def _private_func() -> str:
111
+ return "private"
112
+
113
+ def regular_func(_param: str):
114
+ return "test"
115
+ '''
116
+ result = self._analyze(code, "test_comprehensive.py")
117
+
118
+ # should not flag __future__ imports
119
+ import_names = [i["simple_name"] for i in result["unused_imports"]]
120
+ assert "annotations" not in import_names, "__future__ annotations flagged"
121
+ assert "unused_import" in import_names, "Regular unused import should be flagged"
122
+
123
+ # should not flag _ items
124
+ function_names = [f["name"] for f in result["unused_functions"]]
125
+ underscore_funcs = [name for name in function_names if name.startswith("_")]
126
+ assert len(underscore_funcs) == 0, f"Underscore functions flagged: {underscore_funcs}"
127
+
128
+ # should not flag test methods
129
+ magic_methods = ["setUp", "tearDown", "test_something"]
130
+ flagged_magic = [method for method in magic_methods if method in function_names]
131
+ assert len(flagged_magic) == 0, f"Test methods flagged: {flagged_magic}"
132
+
133
+ # should not flag _ param
134
+ param_names = [p["name"] for p in result["unused_parameters"]]
135
+ underscore_params = [name for name in param_names if name.startswith("_")]
136
+ assert len(underscore_params) == 0, f"Underscore parameters flagged: {underscore_params}"
137
+
138
+ def _analyze(self, code: str, filename: str = "example.py") -> dict:
139
+ with tempfile.TemporaryDirectory() as temp_dir:
140
+ file_path = Path(temp_dir) / filename
141
+ file_path.parent.mkdir(parents=True, exist_ok=True)
142
+ file_path.write_text(code)
143
+
144
+ skylos = Skylos()
145
+ result_json = skylos.analyze(str(temp_dir), thr=60)
146
+ return json.loads(result_json)
147
+
148
+ if __name__ == "__main__":
149
+ pytest.main([__file__, "-v"])