skylos 1.0.11__py3-none-any.whl → 1.1.11__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 ADDED
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env python3
2
+ import pytest
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+ from unittest.mock import Mock, patch
7
+ from collections import defaultdict
8
+
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
+ )
34
+
35
+ class TestParseExcludeFolders:
36
+
37
+ def test_default_exclude_folders_included(self):
38
+ """default folders are included by default."""
39
+ result = parse_exclude_folders(None, use_defaults=True)
40
+ assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
41
+
42
+ def test_default_exclude_folders_disabled(self):
43
+ """default folders can be disabled."""
44
+ result = parse_exclude_folders(None, use_defaults=False)
45
+ assert not DEFAULT_EXCLUDE_FOLDERS.intersection(result)
46
+
47
+ def test_user_exclude_folders_added(self):
48
+ """user-specified folders are added."""
49
+ user_folders = {"custom_folder", "another_folder"}
50
+ result = parse_exclude_folders(user_folders, use_defaults=True)
51
+ assert user_folders.issubset(result)
52
+ assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
53
+
54
+ def test_include_folders_override_defaults(self):
55
+ """include_folders can override defaults."""
56
+ include_folders = {"__pycache__", ".git"}
57
+ result = parse_exclude_folders(None, use_defaults=True, include_folders=include_folders)
58
+ for folder in include_folders:
59
+ assert folder not in result
60
+
61
+ def test_include_folders_override_user_excludes(self):
62
+ """include_folders can override user excludes."""
63
+ user_excludes = {"custom_folder", "another_folder"}
64
+ include_folders = {"custom_folder"}
65
+ result = parse_exclude_folders(user_excludes, use_defaults=False, include_folders=include_folders)
66
+ assert "custom_folder" not in result
67
+ assert "another_folder" in result
68
+
69
+
70
+ class TestSkylos:
71
+
72
+ @pytest.fixture
73
+ def skylos(self):
74
+ return Skylos()
75
+
76
+ def test_init(self, skylos):
77
+ assert skylos.defs == {}
78
+ assert skylos.refs == []
79
+ assert skylos.dynamic == set()
80
+ assert isinstance(skylos.exports, defaultdict)
81
+
82
+ def test_module_name_generation(self, skylos):
83
+ """Test module name generation from file paths."""
84
+ root = Path("/project")
85
+
86
+ # test a regular Python file
87
+ file_path = Path("/project/src/module.py")
88
+ result = skylos._module(root, file_path)
89
+ assert result == "src.module"
90
+
91
+ # test __init__.py file
92
+ file_path = Path("/project/src/__init__.py")
93
+ result = skylos._module(root, file_path)
94
+ assert result == "src"
95
+
96
+ # nested module
97
+ file_path = Path("/project/src/package/submodule.py")
98
+ result = skylos._module(root, file_path)
99
+ assert result == "src.package.submodule"
100
+
101
+ # root level file
102
+ file_path = Path("/project/main.py")
103
+ result = skylos._module(root, file_path)
104
+ assert result == "main"
105
+
106
+ def test_should_exclude_file(self, skylos):
107
+ """
108
+ should exclude pycache, build, egg-info and whatever is in exclude_folders
109
+ """
110
+ root = Path("/project")
111
+ exclude_folders = {"__pycache__", "build", "*.egg-info"}
112
+
113
+ file_path = Path("/project/src/__pycache__/module.pyc")
114
+ assert skylos._should_exclude_file(file_path, root, exclude_folders)
115
+
116
+ file_path = Path("/project/build/lib/module.py")
117
+ assert skylos._should_exclude_file(file_path, root, exclude_folders)
118
+
119
+ file_path = Path("/project/mypackage.egg-info/PKG-INFO")
120
+ assert skylos._should_exclude_file(file_path, root, exclude_folders)
121
+
122
+ file_path = Path("/project/src/module.py")
123
+ assert not skylos._should_exclude_file(file_path, root, exclude_folders)
124
+
125
+ assert not skylos._should_exclude_file(file_path, root, None)
126
+
127
+ @patch('skylos.analyzer.Path')
128
+ def test_get_python_files_single_file(self, mock_path, skylos):
129
+ mock_file = Mock()
130
+ mock_file.is_file.return_value = True
131
+ mock_file.parent = Path("/project")
132
+ mock_path.return_value.resolve.return_value = mock_file
133
+
134
+ files, root = skylos._get_python_files("/project/test.py")
135
+ assert files == [mock_file]
136
+ assert root == Path("/project")
137
+
138
+ @patch('skylos.analyzer.Path')
139
+ def test_get_python_files_directory(self, mock_path, skylos):
140
+ mock_dir = Mock()
141
+ mock_dir.is_file.return_value = False
142
+ mock_files = [Path("/project/file1.py"), Path("/project/file2.py")]
143
+ mock_dir.glob.return_value = mock_files
144
+ mock_path.return_value.resolve.return_value = mock_dir
145
+
146
+ files, root = skylos._get_python_files("/project")
147
+ assert files == mock_files
148
+ assert root == mock_dir
149
+
150
+ def test_mark_exports_in_init(self, skylos):
151
+ mock_def1 = Mock()
152
+ mock_def1.in_init = True
153
+ mock_def1.simple_name = "public_function"
154
+ mock_def1.is_exported = False
155
+
156
+ mock_def2 = Mock()
157
+ mock_def2.in_init = True
158
+ mock_def2.simple_name = "_private_function"
159
+ mock_def2.is_exported = False
160
+
161
+ skylos.defs = {
162
+ "module.public_function": mock_def1,
163
+ "module._private_function": mock_def2
164
+ }
165
+
166
+ skylos._mark_exports()
167
+
168
+ assert mock_def1.is_exported == True
169
+ assert mock_def2.is_exported == False
170
+
171
+ def test_mark_exports_explicit_exports(self, skylos):
172
+ mock_def = Mock()
173
+ mock_def.simple_name = "my_function"
174
+ mock_def.type = "function"
175
+ mock_def.is_exported = False
176
+
177
+ skylos.defs = {"module.my_function": mock_def}
178
+ skylos.exports = {"module": {"my_function"}}
179
+
180
+ skylos._mark_exports()
181
+
182
+ assert mock_def.is_exported == True
183
+
184
+ def test_mark_refs_direct_reference(self, skylos):
185
+ mock_def = Mock()
186
+ mock_def.references = 0
187
+
188
+ skylos.defs = {"module.function": mock_def}
189
+ skylos.refs = [("module.function", None)]
190
+
191
+ skylos._mark_refs()
192
+
193
+ assert mock_def.references == 1
194
+
195
+ def test_mark_refs_import_reference(self, skylos):
196
+ mock_import = Mock()
197
+ mock_import.type = "import"
198
+ mock_import.simple_name = "imported_func"
199
+ mock_import.references = 0
200
+
201
+ mock_original = Mock()
202
+ mock_original.type = "function"
203
+ mock_original.simple_name = "imported_func"
204
+ mock_original.references = 0
205
+
206
+ skylos.defs = {
207
+ "module.imported_func": mock_import,
208
+ "other_module.imported_func": mock_original
209
+ }
210
+ skylos.refs = [("module.imported_func", None)]
211
+
212
+ skylos._mark_refs()
213
+
214
+ assert mock_import.references == 1
215
+ assert mock_original.references == 1
216
+
217
+
218
+ class TestHeuristics:
219
+
220
+ @pytest.fixture
221
+ def skylos_with_class_methods(self, mock_definition):
222
+ skylos = Skylos()
223
+
224
+ mock_class = mock_definition(
225
+ name="MyClass",
226
+ simple_name="MyClass",
227
+ type="class",
228
+ references=1
229
+ )
230
+
231
+ mock_init = mock_definition(
232
+ name="MyClass.__init__",
233
+ simple_name="__init__",
234
+ type="method",
235
+ references=0
236
+ )
237
+
238
+ mock_enter = mock_definition(
239
+ name="MyClass.__enter__",
240
+ simple_name="__enter__",
241
+ type="method",
242
+ references=0
243
+ )
244
+
245
+ skylos.defs = {
246
+ "MyClass": mock_class,
247
+ "MyClass.__init__": mock_init,
248
+ "MyClass.__enter__": mock_enter
249
+ }
250
+
251
+ return skylos, mock_class, mock_init, mock_enter
252
+
253
+ def test_auto_called_methods_get_references(self, skylos_with_class_methods):
254
+ """auto-called methods get reference counts when class is used."""
255
+ skylos, mock_class, mock_init, mock_enter = skylos_with_class_methods
256
+
257
+ skylos._apply_heuristics()
258
+
259
+ assert mock_init.references == 1
260
+ 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
+
335
+ class TestAnalyze:
336
+
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
+ @patch('skylos.analyzer.proc_file')
392
+ def test_analyze_basic(self, mock_proc_file, temp_python_project):
393
+ mock_def = Mock()
394
+ mock_def.name = "test.unused_function"
395
+ mock_def.references = 0
396
+ mock_def.is_exported = False
397
+ mock_def.confidence = 80
398
+ mock_def.type = "function"
399
+ mock_def.to_dict.return_value = {
400
+ "name": "test.unused_function",
401
+ "type": "function",
402
+ "file": "test.py",
403
+ "line": 1
404
+ }
405
+
406
+ mock_proc_file.return_value = ([mock_def], [], set(), set())
407
+
408
+ result_json = analyze(str(temp_python_project), conf=60)
409
+ result = json.loads(result_json)
410
+
411
+ assert "unused_functions" in result
412
+ assert "unused_imports" in result
413
+ assert "unused_classes" in result
414
+ assert "unused_variables" in result
415
+ assert "unused_parameters" in result
416
+ assert "analysis_summary" in result
417
+
418
+ def test_analyze_with_exclusions(self, temp_python_project):
419
+ """analyze with folder exclusions."""
420
+ exclude_dir = temp_python_project / "build"
421
+ exclude_dir.mkdir()
422
+ exclude_file = exclude_dir / "generated.py"
423
+ exclude_file.write_text("def generated_function(): pass")
424
+
425
+ result_json = analyze(str(temp_python_project), exclude_folders=["build"]) # Use list instead of set
426
+ result = json.loads(result_json)
427
+
428
+ assert result["analysis_summary"]["excluded_folders"] == ["build"]
429
+
430
+ def test_analyze_empty_directory(self):
431
+ with tempfile.TemporaryDirectory() as temp_dir:
432
+ result_json = analyze(temp_dir, conf=60)
433
+ result = json.loads(result_json)
434
+
435
+ assert result["analysis_summary"]["total_files"] == 0
436
+ assert all(len(result[key]) == 0 for key in [
437
+ "unused_functions", "unused_imports", "unused_classes",
438
+ "unused_variables", "unused_parameters"
439
+ ])
440
+
441
+ def test_confidence_threshold_filtering(self, mock_definition):
442
+ """confidence threshold properly filters results."""
443
+ skylos = Skylos()
444
+
445
+ high_conf = mock_definition(
446
+ name="high_conf",
447
+ simple_name="high_conf",
448
+ type="function",
449
+ references=0,
450
+ is_exported=False,
451
+ confidence=80
452
+ )
453
+
454
+ low_conf = mock_definition(
455
+ name="low_conf",
456
+ simple_name="low_conf",
457
+ type="function",
458
+ references=0,
459
+ is_exported=False,
460
+ confidence=40
461
+ )
462
+
463
+ skylos.defs = {"high_conf": high_conf, "low_conf": low_conf}
464
+
465
+ with patch.object(skylos, '_get_python_files') as mock_get_files:
466
+ mock_get_files.return_value = ([Path("/fake/file.py")], Path("/"))
467
+
468
+ with patch('skylos.analyzer.proc_file') as mock_proc_file:
469
+ mock_proc_file.return_value = ([], [], set(), set())
470
+
471
+ result_json = skylos.analyze("/fake/path", thr=60)
472
+ result = json.loads(result_json)
473
+
474
+ # include only high confidence
475
+ assert len(result["unused_functions"]) == 1
476
+ assert result["unused_functions"][0]["name"] == "high_conf"
477
+
478
+
479
+ class TestProcFile:
480
+
481
+ def test_proc_file_with_valid_python(self):
482
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
483
+ f.write("""
484
+ def test_function():
485
+ pass
486
+
487
+ class TestClass:
488
+ def method(self):
489
+ pass
490
+ """)
491
+ f.flush()
492
+
493
+ try:
494
+ with patch('skylos.analyzer.Visitor') as mock_visitor_class:
495
+ mock_visitor = Mock()
496
+ mock_visitor.defs = []
497
+ mock_visitor.refs = []
498
+ mock_visitor.dyn = set()
499
+ mock_visitor.exports = set()
500
+ mock_visitor_class.return_value = mock_visitor
501
+
502
+ defs, refs, dyn, exports = proc_file(f.name, "test_module")
503
+
504
+ mock_visitor_class.assert_called_once_with("test_module", f.name)
505
+ mock_visitor.visit.assert_called_once()
506
+
507
+ assert defs == []
508
+ assert refs == []
509
+ assert dyn == set()
510
+ assert exports == set()
511
+ finally:
512
+ Path(f.name).unlink()
513
+
514
+ def test_proc_file_with_invalid_python(self):
515
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
516
+ f.write("def invalid_syntax(:\npass")
517
+ f.flush()
518
+
519
+ try:
520
+ defs, refs, dyn, exports = proc_file(f.name, "test_module")
521
+
522
+ assert defs == []
523
+ assert refs == []
524
+ assert dyn == set()
525
+ assert exports == set()
526
+ finally:
527
+ Path(f.name).unlink()
528
+
529
+ def test_proc_file_with_tuple_args(self):
530
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
531
+ f.write("def test(): pass")
532
+ f.flush()
533
+
534
+ try:
535
+ with patch('skylos.analyzer.Visitor') as mock_visitor_class:
536
+ mock_visitor = Mock()
537
+ mock_visitor.defs = []
538
+ mock_visitor.refs = []
539
+ mock_visitor.dyn = set()
540
+ mock_visitor.exports = set()
541
+ mock_visitor_class.return_value = mock_visitor
542
+
543
+ defs, refs, dyn, exports = proc_file((f.name, "test_module"))
544
+
545
+ mock_visitor_class.assert_called_once_with("test_module", f.name)
546
+ finally:
547
+ Path(f.name).unlink()
548
+
549
+
550
+ class TestConstants:
551
+
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
557
+
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
564
+
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
+
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)
582
+
583
+ if __name__ == "__main__":
584
+ pytest.main([__file__, "-v"])