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.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +108 -9
- skylos/cli.py +63 -4
- skylos/visitor.py +5 -7
- {skylos-1.0.11.dist-info → skylos-1.1.11.dist-info}/METADATA +1 -1
- skylos-1.1.11.dist-info/RECORD +25 -0
- test/conftest.py +212 -0
- test/test_analyzer.py +584 -0
- test/test_cli.py +353 -0
- test/test_integration.py +320 -0
- test/test_visitor.py +516 -22
- skylos-1.0.11.dist-info/RECORD +0 -30
- test/pykomodo/__init__.py +0 -0
- test/pykomodo/command_line.py +0 -176
- test/pykomodo/config.py +0 -20
- test/pykomodo/core.py +0 -121
- test/pykomodo/dashboard.py +0 -608
- test/pykomodo/enhanced_chunker.py +0 -304
- test/pykomodo/multi_dirs_chunker.py +0 -783
- test/pykomodo/pykomodo_config.py +0 -68
- test/pykomodo/token_chunker.py +0 -470
- {skylos-1.0.11.dist-info → skylos-1.1.11.dist-info}/WHEEL +0 -0
- {skylos-1.0.11.dist-info → skylos-1.1.11.dist-info}/entry_points.txt +0 -0
- {skylos-1.0.11.dist-info → skylos-1.1.11.dist-info}/top_level.txt +0 -0
test/test_cli.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import pytest
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from unittest.mock import Mock, patch, mock_open
|
|
7
|
+
|
|
8
|
+
from skylos.cli import Colors, CleanFormatter, setup_logger, remove_unused_import, remove_unused_function, interactive_selection, print_badge, main
|
|
9
|
+
|
|
10
|
+
class TestColors:
|
|
11
|
+
def test_colors_defined(self):
|
|
12
|
+
"""Test that all color constants are defined."""
|
|
13
|
+
assert hasattr(Colors, 'RED')
|
|
14
|
+
assert hasattr(Colors, 'GREEN')
|
|
15
|
+
assert hasattr(Colors, 'RESET')
|
|
16
|
+
assert hasattr(Colors, 'BOLD')
|
|
17
|
+
|
|
18
|
+
assert Colors.RED.startswith('\033[')
|
|
19
|
+
assert Colors.RESET == '\033[0m'
|
|
20
|
+
|
|
21
|
+
class TestCleanFormatter:
|
|
22
|
+
def test_clean_formatter_removes_metadata(self):
|
|
23
|
+
"""Test that CleanFormatter only returns the message."""
|
|
24
|
+
formatter = CleanFormatter()
|
|
25
|
+
|
|
26
|
+
record = Mock()
|
|
27
|
+
record.getMessage.return_value = "Test message"
|
|
28
|
+
|
|
29
|
+
result = formatter.format(record)
|
|
30
|
+
assert result == "Test message"
|
|
31
|
+
|
|
32
|
+
record.getMessage.assert_called_once()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestSetupLogger:
|
|
36
|
+
|
|
37
|
+
@patch('skylos.cli.logging.FileHandler')
|
|
38
|
+
@patch('skylos.cli.logging.StreamHandler')
|
|
39
|
+
def test_setup_logger_console_only(self, mock_stream_handler, mock_file_handler):
|
|
40
|
+
"""Test logger setup without output file."""
|
|
41
|
+
mock_handler = Mock()
|
|
42
|
+
mock_stream_handler.return_value = mock_handler
|
|
43
|
+
|
|
44
|
+
logger = setup_logger()
|
|
45
|
+
|
|
46
|
+
assert logger.name == 'skylos'
|
|
47
|
+
assert logger.level == logging.INFO
|
|
48
|
+
assert not logger.propagate
|
|
49
|
+
|
|
50
|
+
mock_stream_handler.assert_called_once_with(sys.stdout)
|
|
51
|
+
mock_file_handler.assert_not_called()
|
|
52
|
+
|
|
53
|
+
@patch('skylos.cli.logging.FileHandler')
|
|
54
|
+
@patch('skylos.cli.logging.StreamHandler')
|
|
55
|
+
def test_setup_logger_with_output_file(self, mock_stream_handler, mock_file_handler):
|
|
56
|
+
"""Test logger setup with output file."""
|
|
57
|
+
mock_stream_handler.return_value = Mock()
|
|
58
|
+
mock_file_handler.return_value = Mock()
|
|
59
|
+
|
|
60
|
+
logger = setup_logger("output.log")
|
|
61
|
+
|
|
62
|
+
mock_stream_handler.assert_called_once_with(sys.stdout)
|
|
63
|
+
mock_file_handler.assert_called_once_with("output.log")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestRemoveUnusedImport:
|
|
67
|
+
"""Test unused import removal functionality."""
|
|
68
|
+
|
|
69
|
+
def test_remove_simple_import(self):
|
|
70
|
+
"""Test removing a simple import statement."""
|
|
71
|
+
content = """import os
|
|
72
|
+
import sys
|
|
73
|
+
import json
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
print(sys.version)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
with patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
80
|
+
result = remove_unused_import("test.py", "os", 1)
|
|
81
|
+
|
|
82
|
+
assert result is True
|
|
83
|
+
handle = mock_file()
|
|
84
|
+
written_content = ''.join(call.args[0] for call in handle.write.call_args_list)
|
|
85
|
+
assert "import os" not in written_content
|
|
86
|
+
|
|
87
|
+
def test_remove_from_multi_import(self):
|
|
88
|
+
content = "import os, sys, json\n"
|
|
89
|
+
|
|
90
|
+
with patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
91
|
+
result = remove_unused_import("test.py", "os", 1)
|
|
92
|
+
|
|
93
|
+
assert result is True
|
|
94
|
+
|
|
95
|
+
def test_remove_from_import_statement(self):
|
|
96
|
+
content = "from collections import defaultdict, Counter\n"
|
|
97
|
+
|
|
98
|
+
with patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
99
|
+
result = remove_unused_import("test.py", "Counter", 1)
|
|
100
|
+
|
|
101
|
+
assert result is True
|
|
102
|
+
handle = mock_file()
|
|
103
|
+
written_lines = handle.writelines.call_args[0][0]
|
|
104
|
+
written_content = ''.join(written_lines)
|
|
105
|
+
assert "defaultdict" in written_content
|
|
106
|
+
assert "Counter" not in written_content
|
|
107
|
+
|
|
108
|
+
def test_remove_entire_from_import(self):
|
|
109
|
+
content = "from collections import defaultdict\n"
|
|
110
|
+
|
|
111
|
+
with patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
112
|
+
result = remove_unused_import("test.py", "defaultdict", 1)
|
|
113
|
+
|
|
114
|
+
assert result is True
|
|
115
|
+
handle = mock_file()
|
|
116
|
+
written_content = ''.join(call.args[0] for call in handle.write.call_args_list)
|
|
117
|
+
# line should be empty
|
|
118
|
+
assert written_content.strip() == ""
|
|
119
|
+
|
|
120
|
+
def test_remove_import_file_error(self):
|
|
121
|
+
"""handling file errors when removing imports."""
|
|
122
|
+
with patch('builtins.open', side_effect=FileNotFoundError("File not found")):
|
|
123
|
+
result = remove_unused_import("nonexistent.py", "os", 1)
|
|
124
|
+
assert result is False
|
|
125
|
+
|
|
126
|
+
class TestRemoveUnusedFunction:
|
|
127
|
+
def test_remove_simple_function(self):
|
|
128
|
+
"""test remove a simple function."""
|
|
129
|
+
content = """def used_function():
|
|
130
|
+
return "used"
|
|
131
|
+
|
|
132
|
+
def unused_function():
|
|
133
|
+
return "unused"
|
|
134
|
+
|
|
135
|
+
def another_function():
|
|
136
|
+
return "another"
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
with patch('skylos.cli.ast.parse') as mock_parse, \
|
|
140
|
+
patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
141
|
+
|
|
142
|
+
mock_func_node = Mock()
|
|
143
|
+
mock_func_node.name = "unused_function"
|
|
144
|
+
mock_func_node.lineno = 4
|
|
145
|
+
mock_func_node.end_lineno = 5
|
|
146
|
+
mock_func_node.decorator_list = []
|
|
147
|
+
|
|
148
|
+
with patch('skylos.cli.ast.walk', return_value=[mock_func_node]):
|
|
149
|
+
with patch('skylos.cli.ast.FunctionDef', mock_func_node.__class__):
|
|
150
|
+
result = remove_unused_function("test.py", "unused_function", 4)
|
|
151
|
+
|
|
152
|
+
assert result is True
|
|
153
|
+
|
|
154
|
+
def test_remove_function_with_decorators(self):
|
|
155
|
+
"""removing function with decorators."""
|
|
156
|
+
content = """@property
|
|
157
|
+
@decorator
|
|
158
|
+
def unused_function():
|
|
159
|
+
return "unused"
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
with patch('skylos.cli.ast.parse') as mock_parse, \
|
|
163
|
+
patch('builtins.open', mock_open(read_data=content)) as mock_file:
|
|
164
|
+
|
|
165
|
+
mock_decorator = Mock()
|
|
166
|
+
mock_decorator.lineno = 1
|
|
167
|
+
|
|
168
|
+
mock_func_node = Mock()
|
|
169
|
+
mock_func_node.name = "unused_function"
|
|
170
|
+
mock_func_node.lineno = 3
|
|
171
|
+
mock_func_node.end_lineno = 4
|
|
172
|
+
mock_func_node.decorator_list = [mock_decorator]
|
|
173
|
+
|
|
174
|
+
with patch('skylos.cli.ast.walk', return_value=[mock_func_node]):
|
|
175
|
+
with patch('skylos.cli.ast.FunctionDef', mock_func_node.__class__):
|
|
176
|
+
result = remove_unused_function("test.py", "unused_function", 3)
|
|
177
|
+
|
|
178
|
+
assert result is True
|
|
179
|
+
|
|
180
|
+
def test_remove_function_file_error(self):
|
|
181
|
+
with patch('builtins.open', side_effect=FileNotFoundError("File not found")):
|
|
182
|
+
result = remove_unused_function("nonexistent.py", "func", 1)
|
|
183
|
+
assert result is False
|
|
184
|
+
|
|
185
|
+
def test_remove_function_parse_error(self):
|
|
186
|
+
with patch('builtins.open', mock_open(read_data="invalid python code")), \
|
|
187
|
+
patch('skylos.cli.ast.parse', side_effect=SyntaxError("Invalid syntax")):
|
|
188
|
+
result = remove_unused_function("test.py", "func", 1)
|
|
189
|
+
assert result is False
|
|
190
|
+
|
|
191
|
+
class TestInteractiveSelection:
|
|
192
|
+
@pytest.fixture
|
|
193
|
+
def mock_logger(self):
|
|
194
|
+
return Mock()
|
|
195
|
+
|
|
196
|
+
@pytest.fixture
|
|
197
|
+
def sample_unused_items(self):
|
|
198
|
+
"""create fake sample unused items for testing"""
|
|
199
|
+
functions = [
|
|
200
|
+
{"name": "unused_func1", "file": "test1.py", "line": 10},
|
|
201
|
+
{"name": "unused_func2", "file": "test2.py", "line": 20}
|
|
202
|
+
]
|
|
203
|
+
imports = [
|
|
204
|
+
{"name": "unused_import1", "file": "test1.py", "line": 1},
|
|
205
|
+
{"name": "unused_import2", "file": "test2.py", "line": 2}
|
|
206
|
+
]
|
|
207
|
+
return functions, imports
|
|
208
|
+
|
|
209
|
+
def test_interactive_selection_unavailable(self, mock_logger, sample_unused_items):
|
|
210
|
+
"""interactive selection when inquirer is not available."""
|
|
211
|
+
functions, imports = sample_unused_items
|
|
212
|
+
|
|
213
|
+
with patch('skylos.cli.INTERACTIVE_AVAILABLE', False):
|
|
214
|
+
selected_functions, selected_imports = interactive_selection(
|
|
215
|
+
mock_logger, functions, imports
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
assert selected_functions == []
|
|
219
|
+
assert selected_imports == []
|
|
220
|
+
mock_logger.error.assert_called_once()
|
|
221
|
+
|
|
222
|
+
@patch('skylos.cli.inquirer')
|
|
223
|
+
def test_interactive_selection_with_selections(self, mock_inquirer, mock_logger, sample_unused_items):
|
|
224
|
+
functions, imports = sample_unused_items
|
|
225
|
+
|
|
226
|
+
mock_inquirer.prompt.side_effect = [
|
|
227
|
+
{'functions': [functions[0]]},
|
|
228
|
+
{'imports': [imports[1]]}
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
with patch('skylos.cli.INTERACTIVE_AVAILABLE', True):
|
|
232
|
+
selected_functions, selected_imports = interactive_selection(
|
|
233
|
+
mock_logger, functions, imports
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
assert selected_functions == [functions[0]]
|
|
237
|
+
assert selected_imports == [imports[1]]
|
|
238
|
+
assert mock_inquirer.prompt.call_count == 2
|
|
239
|
+
|
|
240
|
+
@patch('skylos.cli.inquirer')
|
|
241
|
+
def test_interactive_selection_no_selections(self, mock_inquirer, mock_logger, sample_unused_items):
|
|
242
|
+
functions, imports = sample_unused_items
|
|
243
|
+
|
|
244
|
+
mock_inquirer.prompt.return_value = None
|
|
245
|
+
|
|
246
|
+
with patch('skylos.cli.INTERACTIVE_AVAILABLE', True):
|
|
247
|
+
selected_functions, selected_imports = interactive_selection(
|
|
248
|
+
mock_logger, functions, imports
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert selected_functions == []
|
|
252
|
+
assert selected_imports == []
|
|
253
|
+
|
|
254
|
+
def test_interactive_selection_empty_lists(self, mock_logger):
|
|
255
|
+
selected_functions, selected_imports = interactive_selection(
|
|
256
|
+
mock_logger, [], []
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert selected_functions == []
|
|
260
|
+
assert selected_imports == []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestPrintBadge:
|
|
264
|
+
@pytest.fixture
|
|
265
|
+
def mock_logger(self):
|
|
266
|
+
return Mock()
|
|
267
|
+
|
|
268
|
+
def test_print_badge_zero_dead_code(self, mock_logger):
|
|
269
|
+
"""Test badge printing with zero dead code."""
|
|
270
|
+
print_badge(0, mock_logger)
|
|
271
|
+
|
|
272
|
+
calls = [call.args[0] for call in mock_logger.info.call_args_list]
|
|
273
|
+
badge_call = next((call for call in calls if "Dead_Code-Free" in call), None)
|
|
274
|
+
assert badge_call is not None
|
|
275
|
+
assert "brightgreen" in badge_call
|
|
276
|
+
|
|
277
|
+
def test_print_badge_with_dead_code(self, mock_logger):
|
|
278
|
+
print_badge(5, mock_logger)
|
|
279
|
+
|
|
280
|
+
calls = [call.args[0] for call in mock_logger.info.call_args_list]
|
|
281
|
+
badge_call = next((call for call in calls if "Dead_Code-5" in call), None)
|
|
282
|
+
assert badge_call is not None
|
|
283
|
+
assert "orange" in badge_call
|
|
284
|
+
|
|
285
|
+
class TestMainFunction:
|
|
286
|
+
@pytest.fixture
|
|
287
|
+
def mock_skylos_result(self):
|
|
288
|
+
return {
|
|
289
|
+
"unused_functions": [
|
|
290
|
+
{"name": "unused_func", "file": "test.py", "line": 10}
|
|
291
|
+
],
|
|
292
|
+
"unused_imports": [
|
|
293
|
+
{"name": "unused_import", "file": "test.py", "line": 1}
|
|
294
|
+
],
|
|
295
|
+
"unused_parameters": [],
|
|
296
|
+
"unused_variables": [],
|
|
297
|
+
"analysis_summary": {
|
|
298
|
+
"total_files": 2,
|
|
299
|
+
"excluded_folders": []
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def test_main_json_output(self, mock_skylos_result):
|
|
304
|
+
"""testing main function with JSON output"""
|
|
305
|
+
test_args = ["cli.py", "test_path", "--json"]
|
|
306
|
+
|
|
307
|
+
with patch('sys.argv', test_args), \
|
|
308
|
+
patch('skylos.cli.skylos.analyze') as mock_analyze, \
|
|
309
|
+
patch('skylos.cli.setup_logger') as mock_setup_logger:
|
|
310
|
+
|
|
311
|
+
mock_logger = Mock()
|
|
312
|
+
mock_setup_logger.return_value = mock_logger
|
|
313
|
+
mock_analyze.return_value = json.dumps(mock_skylos_result)
|
|
314
|
+
|
|
315
|
+
main()
|
|
316
|
+
|
|
317
|
+
mock_analyze.assert_called_once()
|
|
318
|
+
mock_logger.info.assert_called_with(json.dumps(mock_skylos_result))
|
|
319
|
+
|
|
320
|
+
def test_main_verbose_output(self, mock_skylos_result):
|
|
321
|
+
""" with verbose"""
|
|
322
|
+
test_args = ["cli.py", "test_path", "--verbose"]
|
|
323
|
+
|
|
324
|
+
with patch('sys.argv', test_args), \
|
|
325
|
+
patch('skylos.cli.skylos.analyze') as mock_analyze, \
|
|
326
|
+
patch('skylos.cli.setup_logger') as mock_setup_logger:
|
|
327
|
+
|
|
328
|
+
mock_logger = Mock()
|
|
329
|
+
mock_setup_logger.return_value = mock_logger
|
|
330
|
+
mock_analyze.return_value = json.dumps(mock_skylos_result)
|
|
331
|
+
|
|
332
|
+
main()
|
|
333
|
+
|
|
334
|
+
mock_logger.setLevel.assert_called_with(logging.DEBUG)
|
|
335
|
+
|
|
336
|
+
def test_main_analysis_error(self):
|
|
337
|
+
test_args = ["cli.py", "test_path"]
|
|
338
|
+
|
|
339
|
+
with patch('sys.argv', test_args), \
|
|
340
|
+
patch('skylos.cli.skylos.analyze', side_effect=Exception("Analysis failed")), \
|
|
341
|
+
patch('skylos.cli.setup_logger') as mock_setup_logger, \
|
|
342
|
+
patch('skylos.cli.parse_exclude_folders', return_value=set()):
|
|
343
|
+
|
|
344
|
+
mock_logger = Mock()
|
|
345
|
+
mock_setup_logger.return_value = mock_logger
|
|
346
|
+
|
|
347
|
+
with pytest.raises(SystemExit):
|
|
348
|
+
main()
|
|
349
|
+
|
|
350
|
+
mock_logger.error.assert_called_with("Error during analysis: Analysis failed")
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
pytest.main([__file__, "-v"])
|
test/test_integration.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from textwrap import dedent
|
|
7
|
+
|
|
8
|
+
import skylos
|
|
9
|
+
from skylos.analyzer import Skylos
|
|
10
|
+
from skylos.analyzer import DEFAULT_EXCLUDE_FOLDERS
|
|
11
|
+
|
|
12
|
+
class TestSkylosIntegration:
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def temp_project(self):
|
|
16
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
17
|
+
project_path = Path(temp_dir)
|
|
18
|
+
|
|
19
|
+
main_py = project_path / "main.py"
|
|
20
|
+
main_py.write_text(dedent("""
|
|
21
|
+
import os # unused import
|
|
22
|
+
import sys
|
|
23
|
+
from typing import List # unused import
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
from utils import UsedClass, exported_function
|
|
26
|
+
|
|
27
|
+
def used_function(x):
|
|
28
|
+
'''This function is called and should not be flagged'''
|
|
29
|
+
return x * 2
|
|
30
|
+
|
|
31
|
+
def unused_function(a, b):
|
|
32
|
+
'''This function is never called'''
|
|
33
|
+
unused_var = a + b # unused variable
|
|
34
|
+
return unused_var
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
result = used_function(5)
|
|
38
|
+
print(result, file=sys.stderr)
|
|
39
|
+
data = defaultdict(list)
|
|
40
|
+
|
|
41
|
+
obj = UsedClass()
|
|
42
|
+
obj.get_value()
|
|
43
|
+
exported_function()
|
|
44
|
+
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
49
|
+
"""))
|
|
50
|
+
|
|
51
|
+
utils_py = project_path / "utils.py"
|
|
52
|
+
utils_py.write_text(dedent("""
|
|
53
|
+
import json # unused import
|
|
54
|
+
|
|
55
|
+
class UsedClass:
|
|
56
|
+
'''This class is imported and used'''
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.value = 42
|
|
59
|
+
|
|
60
|
+
def get_value(self):
|
|
61
|
+
return self.value
|
|
62
|
+
|
|
63
|
+
class UnusedClass:
|
|
64
|
+
'''This class is never used'''
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.data = {}
|
|
67
|
+
|
|
68
|
+
def unused_method(self, param): # unused parameter
|
|
69
|
+
return "never called"
|
|
70
|
+
|
|
71
|
+
def exported_function():
|
|
72
|
+
'''This function is used by main.py'''
|
|
73
|
+
return "exported"
|
|
74
|
+
"""))
|
|
75
|
+
|
|
76
|
+
package_dir = project_path / "mypackage"
|
|
77
|
+
package_dir.mkdir()
|
|
78
|
+
|
|
79
|
+
init_py = package_dir / "__init__.py"
|
|
80
|
+
init_py.write_text(dedent("""
|
|
81
|
+
from .submodule import PublicClass
|
|
82
|
+
|
|
83
|
+
__all__ = ['PublicClass', 'public_function']
|
|
84
|
+
|
|
85
|
+
def public_function():
|
|
86
|
+
return "public"
|
|
87
|
+
|
|
88
|
+
def _private_function():
|
|
89
|
+
return "private"
|
|
90
|
+
"""))
|
|
91
|
+
|
|
92
|
+
sub_py = package_dir / "submodule.py"
|
|
93
|
+
sub_py.write_text(dedent("""
|
|
94
|
+
class PublicClass:
|
|
95
|
+
'''Exported via __init__.py'''
|
|
96
|
+
def method(self):
|
|
97
|
+
return "method"
|
|
98
|
+
|
|
99
|
+
class InternalClass:
|
|
100
|
+
'''Not exported, should be flagged'''
|
|
101
|
+
pass
|
|
102
|
+
"""))
|
|
103
|
+
|
|
104
|
+
test_py = project_path / "test_example.py"
|
|
105
|
+
test_py.write_text(dedent("""
|
|
106
|
+
import unittest
|
|
107
|
+
from main import used_function
|
|
108
|
+
|
|
109
|
+
class TestExample(unittest.TestCase):
|
|
110
|
+
def test_used_function(self):
|
|
111
|
+
result = used_function(3)
|
|
112
|
+
self.assertEqual(result, 6)
|
|
113
|
+
|
|
114
|
+
def test_unused_method(self):
|
|
115
|
+
pass
|
|
116
|
+
"""))
|
|
117
|
+
|
|
118
|
+
yield project_path
|
|
119
|
+
|
|
120
|
+
def test_basic_analysis(self, temp_project):
|
|
121
|
+
"""Test basic unused code detection"""
|
|
122
|
+
result_json = skylos.analyze(str(temp_project), exclude_folders=list(DEFAULT_EXCLUDE_FOLDERS))
|
|
123
|
+
result = json.loads(result_json)
|
|
124
|
+
|
|
125
|
+
assert "unused_functions" in result
|
|
126
|
+
assert "unused_imports" in result
|
|
127
|
+
assert "unused_variables" in result
|
|
128
|
+
assert "unused_parameters" in result
|
|
129
|
+
assert "unused_classes" in result
|
|
130
|
+
|
|
131
|
+
assert len(result["unused_functions"]) > 0
|
|
132
|
+
assert len(result["unused_imports"]) > 0
|
|
133
|
+
|
|
134
|
+
unused_function_names = [f["name"] for f in result["unused_functions"]]
|
|
135
|
+
assert "unused_function" in unused_function_names
|
|
136
|
+
|
|
137
|
+
assert "used_function" not in unused_function_names
|
|
138
|
+
assert "main" not in unused_function_names
|
|
139
|
+
|
|
140
|
+
def test_unused_imports_detection(self, temp_project):
|
|
141
|
+
"""Test detection of unused imports"""
|
|
142
|
+
result_json = skylos.analyze(str(temp_project))
|
|
143
|
+
result = json.loads(result_json)
|
|
144
|
+
|
|
145
|
+
unused_imports = result["unused_imports"]
|
|
146
|
+
import_names = [imp["name"] for imp in unused_imports]
|
|
147
|
+
|
|
148
|
+
assert any("os" in name for name in import_names)
|
|
149
|
+
assert any("typing.List" in name or "List" in name for name in import_names)
|
|
150
|
+
|
|
151
|
+
used_imports = ["sys", "defaultdict"]
|
|
152
|
+
for used_import in used_imports:
|
|
153
|
+
assert not any(used_import in name for name in import_names)
|
|
154
|
+
|
|
155
|
+
def test_class_detection(self, temp_project):
|
|
156
|
+
"""test detection of unused classes"""
|
|
157
|
+
result_json = skylos.analyze(str(temp_project))
|
|
158
|
+
result = json.loads(result_json)
|
|
159
|
+
|
|
160
|
+
unused_classes = result["unused_classes"]
|
|
161
|
+
class_names = [cls["name"] for cls in unused_classes]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
assert any("UnusedClass" in name for name in class_names), f"UnusedClass not found in {class_names}"
|
|
165
|
+
|
|
166
|
+
used_classes_flagged = [name for name in class_names if "UsedClass" in name]
|
|
167
|
+
assert len(used_classes_flagged) == 0, f"UsedClass was incorrectly flagged as unused: {used_classes_flagged}"
|
|
168
|
+
|
|
169
|
+
# publicclass should not be flagged because it's exported via __init__.py
|
|
170
|
+
public_classes_flagged = [name for name in class_names if "PublicClass" in name]
|
|
171
|
+
assert len(public_classes_flagged) == 0, f"PublicClass was incorrectly flagged as unused: {public_classes_flagged}"
|
|
172
|
+
|
|
173
|
+
def test_exclude_folders(self, temp_project):
|
|
174
|
+
result1 = skylos.analyze(str(temp_project))
|
|
175
|
+
|
|
176
|
+
# exclude the mypackage folder
|
|
177
|
+
result2 = skylos.analyze(str(temp_project), exclude_folders=["mypackage"])
|
|
178
|
+
|
|
179
|
+
# it'll find fewer items when excluding a folder
|
|
180
|
+
data1 = json.loads(result1)
|
|
181
|
+
data2 = json.loads(result2)
|
|
182
|
+
|
|
183
|
+
total1 = sum(len(items) for items in data1.values() if isinstance(items, list))
|
|
184
|
+
total2 = sum(len(items) for items in data2.values() if isinstance(items, list))
|
|
185
|
+
|
|
186
|
+
assert total2 <= total1
|
|
187
|
+
|
|
188
|
+
def test_custom_exclude_folders(self, temp_project):
|
|
189
|
+
custom_dir = temp_project / "custom_exclude"
|
|
190
|
+
custom_dir.mkdir()
|
|
191
|
+
|
|
192
|
+
custom_file = custom_dir / "custom.py"
|
|
193
|
+
custom_file.write_text(dedent("""
|
|
194
|
+
def custom_function():
|
|
195
|
+
return "should be excluded"
|
|
196
|
+
"""))
|
|
197
|
+
|
|
198
|
+
result_json = skylos.analyze(str(temp_project), exclude_folders=["custom_exclude"])
|
|
199
|
+
result = json.loads(result_json)
|
|
200
|
+
|
|
201
|
+
all_files = []
|
|
202
|
+
for category in ["unused_functions", "unused_imports", "unused_classes", "unused_variables"]:
|
|
203
|
+
for item in result[category]:
|
|
204
|
+
all_files.append(item["file"])
|
|
205
|
+
|
|
206
|
+
excluded_files = [f for f in all_files if "custom_exclude" in f]
|
|
207
|
+
assert len(excluded_files) == 0, f"Found excluded files: {excluded_files}"
|
|
208
|
+
|
|
209
|
+
def test_magic_methods_excluded(self, temp_project):
|
|
210
|
+
magic_py = temp_project / "magic.py"
|
|
211
|
+
magic_py.write_text(dedent("""
|
|
212
|
+
class MagicClass:
|
|
213
|
+
def __init__(self):
|
|
214
|
+
self.value = 0
|
|
215
|
+
|
|
216
|
+
def __str__(self):
|
|
217
|
+
return str(self.value)
|
|
218
|
+
|
|
219
|
+
def __len__(self):
|
|
220
|
+
return self.value
|
|
221
|
+
|
|
222
|
+
def regular_method(self):
|
|
223
|
+
return "regular"
|
|
224
|
+
"""))
|
|
225
|
+
|
|
226
|
+
result_json = skylos.analyze(str(temp_project))
|
|
227
|
+
result = json.loads(result_json)
|
|
228
|
+
|
|
229
|
+
unused_functions = result["unused_functions"]
|
|
230
|
+
function_names = [f["name"] for f in unused_functions]
|
|
231
|
+
|
|
232
|
+
magic_methods = ["__init__", "__str__", "__len__"]
|
|
233
|
+
for magic_method in magic_methods:
|
|
234
|
+
assert not any(magic_method in name for name in function_names)
|
|
235
|
+
|
|
236
|
+
def test_test_methods_excluded(self, temp_project):
|
|
237
|
+
"""Test that test methods are not flagged as unused"""
|
|
238
|
+
result_json = skylos.analyze(str(temp_project))
|
|
239
|
+
result = json.loads(result_json)
|
|
240
|
+
|
|
241
|
+
unused_functions = result["unused_functions"]
|
|
242
|
+
function_names = [f["name"] for f in unused_functions]
|
|
243
|
+
|
|
244
|
+
test_methods = ["test_used_function", "test_unused_method"]
|
|
245
|
+
for test_method in test_methods:
|
|
246
|
+
assert not any(test_method in name for name in function_names)
|
|
247
|
+
|
|
248
|
+
def test_exported_functions_excluded(self, temp_project):
|
|
249
|
+
result_json = skylos.analyze(str(temp_project))
|
|
250
|
+
result = json.loads(result_json)
|
|
251
|
+
|
|
252
|
+
unused_functions = result["unused_functions"]
|
|
253
|
+
function_names = [f["name"] for f in unused_functions]
|
|
254
|
+
|
|
255
|
+
assert not any("public_function" in name for name in function_names)
|
|
256
|
+
|
|
257
|
+
def test_single_file_analysis(self, temp_project):
|
|
258
|
+
main_file = temp_project / "main.py"
|
|
259
|
+
|
|
260
|
+
result_json = skylos.analyze(str(main_file))
|
|
261
|
+
result = json.loads(result_json)
|
|
262
|
+
|
|
263
|
+
# should still detect unused items in the single file
|
|
264
|
+
assert len(result["unused_functions"]) > 0
|
|
265
|
+
assert len(result["unused_imports"]) > 0
|
|
266
|
+
|
|
267
|
+
all_files = set()
|
|
268
|
+
for category in ["unused_functions", "unused_imports", "unused_variables"]:
|
|
269
|
+
for item in result[category]:
|
|
270
|
+
all_files.add(Path(item["file"]).name)
|
|
271
|
+
|
|
272
|
+
assert "main.py" in all_files
|
|
273
|
+
assert "utils.py" not in all_files
|
|
274
|
+
|
|
275
|
+
def test_confidence_levels(self, temp_project):
|
|
276
|
+
result_json = skylos.analyze(str(temp_project))
|
|
277
|
+
result = json.loads(result_json)
|
|
278
|
+
|
|
279
|
+
for category in ["unused_functions", "unused_imports", "unused_variables"]:
|
|
280
|
+
for item in result[category]:
|
|
281
|
+
assert "confidence" in item
|
|
282
|
+
assert isinstance(item["confidence"], (int, float))
|
|
283
|
+
assert 0 <= item["confidence"] <= 100
|
|
284
|
+
|
|
285
|
+
def test_analyzer_class_direct(self, temp_project):
|
|
286
|
+
analyzer = Skylos()
|
|
287
|
+
result_json = analyzer.analyze(str(temp_project), thr=50)
|
|
288
|
+
result = json.loads(result_json)
|
|
289
|
+
|
|
290
|
+
expected_keys = [
|
|
291
|
+
"unused_functions", "unused_imports", "unused_classes",
|
|
292
|
+
"unused_variables", "unused_parameters", "analysis_summary"
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
for key in expected_keys:
|
|
296
|
+
assert key in result
|
|
297
|
+
|
|
298
|
+
def test_empty_project(self):
|
|
299
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
300
|
+
result_json = skylos.analyze(temp_dir)
|
|
301
|
+
result = json.loads(result_json)
|
|
302
|
+
|
|
303
|
+
assert result["unused_functions"] == []
|
|
304
|
+
assert result["unused_imports"] == []
|
|
305
|
+
assert result["analysis_summary"]["total_files"] == 0
|
|
306
|
+
|
|
307
|
+
def test_threshold_filtering(self, temp_project):
|
|
308
|
+
"""Test that confidence threshold filtering works"""
|
|
309
|
+
# high threshold = find fewer items
|
|
310
|
+
result_high = json.loads(skylos.analyze(str(temp_project), conf=95))
|
|
311
|
+
|
|
312
|
+
# low threshold = should find more items
|
|
313
|
+
result_low = json.loads(skylos.analyze(str(temp_project), conf=10))
|
|
314
|
+
|
|
315
|
+
assert len(result_high["unused_functions"]) <= len(result_low["unused_functions"])
|
|
316
|
+
assert len(result_high["unused_imports"]) <= len(result_low["unused_imports"])
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
pytest.main([__file__, "-v"])
|