skylos 1.1.12__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.

@@ -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"])
test/test_constants.py ADDED
@@ -0,0 +1,348 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from skylos.constants import (
4
+ PENALTIES,
5
+ TEST_FILE_RE,
6
+ TEST_IMPORT_RE,
7
+ TEST_DECOR_RE,
8
+ AUTO_CALLED,
9
+ TEST_METHOD_PATTERN,
10
+ UNITTEST_LIFECYCLE_METHODS,
11
+ FRAMEWORK_FILE_RE,
12
+ DEFAULT_EXCLUDE_FOLDERS,
13
+ is_test_path, is_framework_path)
14
+
15
+ class TestTestFileRegex:
16
+
17
+ def test_test_file_patterns_match(self):
18
+ test_paths = [
19
+ "module_test.py", # ending with _test.py
20
+ "tests/test_something.py", # in the tests dir
21
+ "test/unit_tests.py", # in test dir
22
+ "/path/to/tests/helper.py", # in tests dir
23
+ "src/tests/integration/test_api.py", # also in tests dir
24
+ "Tests/TestCase.py", # test with captal T
25
+ "TESTS/MODULE_TEST.PY", # all caps tests dir
26
+ "/project/test/conftest.py",
27
+ "C:\\project\\tests\\test_file.py", # windows path
28
+ "project\\test\\helper.py",
29
+ "some_module_test.py"
30
+ ]
31
+
32
+ for path in test_paths:
33
+ assert TEST_FILE_RE.search(path), f"Should match test path: {path}"
34
+
35
+ def test_non_test_file_patterns_dont_match(self):
36
+ """Test that TEST_FILE_RE doesn't match non-test files"""
37
+ non_test_paths = [
38
+ "test_module.py",
39
+ "module.py",
40
+ "main.py",
41
+ "src/utils.py",
42
+ "app/models.py",
43
+ "testing_utils.py",
44
+ "contest.py",
45
+ "protest.py",
46
+ "/path/to/contest_module.py",
47
+ "mytests.py",
48
+ "src/testing/helper.py",
49
+ "testmodule.py"
50
+ ]
51
+
52
+ for path in non_test_paths:
53
+ assert not TEST_FILE_RE.search(path), f"Should NOT match non-test path: {path}"
54
+
55
+
56
+ class TestTestImportRegex:
57
+
58
+ def test_test_import_patterns_match(self):
59
+ test_imports = [
60
+ "pytest",
61
+ "pytest.fixture",
62
+ "pytest.mark.parametrize",
63
+ "unittest",
64
+ "unittest.mock",
65
+ "unittest.TestCase",
66
+ "nose",
67
+ "nose.tools",
68
+ "mock",
69
+ "mock.patch",
70
+ "responses",
71
+ "responses.activate"
72
+ ]
73
+
74
+ for import_name in test_imports:
75
+ assert TEST_IMPORT_RE.match(import_name), f"Should match test import: {import_name}"
76
+
77
+ def test_non_test_import_patterns_dont_match(self):
78
+ non_test_imports = [
79
+ "os",
80
+ "sys",
81
+ "json",
82
+ "requests",
83
+ "flask",
84
+ "django",
85
+ "numpy",
86
+ "pandas",
87
+ "pytest_plugin",
88
+ "unittest_extensions",
89
+ "mockery",
90
+ "response"
91
+ ]
92
+
93
+ for import_name in non_test_imports:
94
+ assert not TEST_IMPORT_RE.match(import_name), f"Should NOT match non-test import: {import_name}"
95
+
96
+ class TestTestDecoratorRegex:
97
+
98
+ def test_test_decorator_patterns_match(self):
99
+ test_decorators = [
100
+ "pytest.fixture",
101
+ "pytest.mark",
102
+ "patch",
103
+ "responses.activate",
104
+ "freeze_time"
105
+ ]
106
+
107
+ for decorator in test_decorators:
108
+ assert TEST_DECOR_RE.match(decorator), f"Should match test decorator: {decorator}"
109
+
110
+ def test_non_test_decorator_patterns_dont_match(self):
111
+ non_test_decorators = [
112
+ "property",
113
+ "staticmethod",
114
+ "classmethod",
115
+ "app.route",
116
+ "login_required",
117
+ "cache.cached",
118
+ "pytest_configure",
119
+ "patcher",
120
+ "response",
121
+ "freeze"
122
+ ]
123
+
124
+ for decorator in non_test_decorators:
125
+ assert not TEST_DECOR_RE.match(decorator), f"Should not match non-test decorator: {decorator}"
126
+
127
+
128
+ class TestTestMethodPattern:
129
+
130
+ def test_test_method_pattern_matches(self):
131
+ """Test that TEST_METHOD_PATTERN matches test method names"""
132
+ test_methods = [
133
+ "test_something",
134
+ "test_user_creation",
135
+ "test_api_response",
136
+ "test_edge_case",
137
+ "test_123",
138
+ "test_with_numbers_123",
139
+ "test_UPPERCASE"
140
+ ]
141
+
142
+ for method in test_methods:
143
+ assert TEST_METHOD_PATTERN.match(method), f"Should match test method: {method}"
144
+
145
+ def test_test_method_pattern_doesnt_match(self):
146
+ non_test_methods = [
147
+ "test", # No _
148
+ "test_",
149
+ "testing_something",
150
+ # dont start with test
151
+ "my_test_method",
152
+ "testSomething",
153
+ "Test_something",
154
+ "_test_something"
155
+ ]
156
+
157
+ for method in non_test_methods:
158
+ assert not TEST_METHOD_PATTERN.match(method), f"Should NOT match non-test method: {method}"
159
+
160
+
161
+ class TestFrameworkFileRegex:
162
+
163
+ def test_framework_file_patterns_match(self):
164
+ framework_files = [
165
+ "views.py",
166
+ "handlers.py",
167
+ "endpoints.py",
168
+ "routes.py",
169
+ "api.py",
170
+ "/path/to/views.py",
171
+ "app/views.py",
172
+ "VIEWS.PY",
173
+ "API.py",
174
+ "C:\\project\\app\\handlers.py",
175
+ "my_handlers.py",
176
+ "user_api.py",
177
+ "admin_views.py"
178
+ ]
179
+
180
+ for file_path in framework_files:
181
+ assert FRAMEWORK_FILE_RE.search(file_path), f"Should match framework file: {file_path}"
182
+
183
+ def test_non_framework_file_patterns_dont_match(self):
184
+ """Test that FRAMEWORK_FILE_RE doesn't match non-framework files"""
185
+ non_framework_files = [
186
+ "models.py",
187
+ "utils.py",
188
+ "config.py",
189
+ "main.py",
190
+ "views_helper.py",
191
+ "endpoint_utils.py",
192
+ "router.py",
193
+ "apis.py",
194
+ "handler.py"
195
+ ]
196
+
197
+ for file_path in non_framework_files:
198
+ assert not FRAMEWORK_FILE_RE.search(file_path), f"Should NOT match non-framework file: {file_path}"
199
+
200
+
201
+ class TestHelperFunctions:
202
+
203
+ def test_is_test_path_with_strings(self):
204
+ assert is_test_path("module_test.py") == True
205
+ assert is_test_path("tests/helper.py") == True
206
+ assert is_test_path("test/conftest.py") == True
207
+ assert is_test_path("test_module.py") == False
208
+ assert is_test_path("regular_module.py") == False
209
+ assert is_test_path("testing_utils.py") == False
210
+
211
+ def test_is_test_path_with_path_objects(self):
212
+ assert is_test_path(Path("module_test.py")) == True
213
+ assert is_test_path(Path("tests/helper.py")) == True
214
+ assert is_test_path(Path("test_module.py")) == False
215
+ assert is_test_path(Path("regular_module.py")) == False
216
+
217
+ def test_is_framework_path_with_strings(self):
218
+ assert is_framework_path("views.py") == True
219
+ assert is_framework_path("handlers.py") == True
220
+ assert is_framework_path("api.py") == True
221
+ assert is_framework_path("models.py") == False
222
+ assert is_framework_path("utils.py") == False
223
+
224
+ def test_is_framework_path_with_path_objects(self):
225
+ assert is_framework_path(Path("views.py")) == True
226
+ assert is_framework_path(Path("app/handlers.py")) == True
227
+ assert is_framework_path(Path("models.py")) == False
228
+
229
+ def test_path_functions_with_complex_paths(self):
230
+ complex_paths = [
231
+ ("/project/src/tests/unit/test_models.py", True, False),
232
+ ("/project/app/views.py", False, True),
233
+ ("/project/src/utils/helpers.py", False, False),
234
+ ("C:\\project\\tests\\integration\\test_api.py", True, True),
235
+ ("C:\\project\\app\\api\\handlers.py", False, True),
236
+ ("/very/deep/path/to/tests/conftest.py", True, False),
237
+ ("/app/endpoints.py", False, True),
238
+ ("/project/module_test.py", True, False)
239
+ ]
240
+
241
+ for path, is_test, is_framework in complex_paths:
242
+ assert is_test_path(path) == is_test, f"is_test_path({path}) should be {is_test}"
243
+ assert is_framework_path(path) == is_framework, f"is_framework_path({path}) should be {is_framework}"
244
+
245
+
246
+ class TestConstants:
247
+
248
+ def test_penalties_structure(self):
249
+ """Test that penalities contains expected keys and reasonable values"""
250
+ expected_keys = {
251
+ "private_name", "dunder_or_magic", "underscored_var",
252
+ "in_init_file", "dynamic_module", "test_related", "framework_magic"
253
+ }
254
+ assert set(PENALTIES.keys()) == expected_keys
255
+
256
+ for key, value in PENALTIES.items():
257
+ assert isinstance(value, int), f"Penalty {key} should be int, got {type(value)}"
258
+ assert value > 0, f"Penalty {key} should be positive, got {value}"
259
+ assert value <= 100, f"Penalty {key} should be <= 100, got {value}"
260
+
261
+ def test_auto_called_methods(self):
262
+ expected_methods = {"__init__", "__enter__", "__exit__"}
263
+ assert AUTO_CALLED == expected_methods
264
+
265
+ for method in AUTO_CALLED:
266
+ assert method.startswith("__") and method.endswith("__"), f"{method} should be dunder method"
267
+
268
+ def test_unittest_lifecycle_methods(self):
269
+ expected_methods = {
270
+ 'setUp', 'tearDown', 'setUpClass', 'tearDownClass',
271
+ 'setUpModule', 'tearDownModule'
272
+ }
273
+ assert UNITTEST_LIFECYCLE_METHODS == expected_methods
274
+
275
+ setup_methods = {m for m in UNITTEST_LIFECYCLE_METHODS if 'setUp' in m or 'setup' in m.lower()}
276
+ teardown_methods = {m for m in UNITTEST_LIFECYCLE_METHODS if 'tearDown' in m or 'teardown' in m.lower()}
277
+
278
+ assert len(setup_methods) == 3, f"Should have 3 setup methods, got {setup_methods}"
279
+ assert len(teardown_methods) == 3, f"Should have 3 teardown methods, got {teardown_methods}"
280
+
281
+ def test_default_exclude_folders(self):
282
+ expected_folders = {
283
+ "__pycache__", ".git", ".pytest_cache", ".mypy_cache", ".tox",
284
+ "htmlcov", ".coverage", "build", "dist", "*.egg-info", "venv", ".venv"
285
+ }
286
+ assert expected_folders.issubset(DEFAULT_EXCLUDE_FOLDERS)
287
+
288
+ for folder in DEFAULT_EXCLUDE_FOLDERS:
289
+ assert isinstance(folder, str), f"Folder {folder} should be string"
290
+ assert len(folder) > 0, f"Folder name should not be empty"
291
+
292
+
293
+ class TestRegexEdgeCases:
294
+
295
+ def test_test_file_regex_case_insensitivity(self):
296
+ assert TEST_FILE_RE.search("MODULE_TEST.PY")
297
+ assert TEST_FILE_RE.search("Tests/Helper.py")
298
+ assert TEST_FILE_RE.search("TEST/CONFTEST.PY")
299
+ assert TEST_FILE_RE.search("tests/MODULE.PY")
300
+
301
+ def test_test_file_regex_path_separators(self):
302
+ # unix
303
+ assert TEST_FILE_RE.search("tests/unit/test_module.py")
304
+ assert TEST_FILE_RE.search("/absolute/path/tests/test_file.py")
305
+
306
+ # windows
307
+ assert TEST_FILE_RE.search("tests\\unit\\test_module.py")
308
+ assert TEST_FILE_RE.search("C:\\project\\test\\test_file.py")
309
+
310
+ assert TEST_FILE_RE.search("project/tests\\test_file.py")
311
+
312
+ def test_framework_file_regex_case_insensitivity(self):
313
+ assert FRAMEWORK_FILE_RE.search("VIEWS.PY")
314
+ assert FRAMEWORK_FILE_RE.search("Handlers.py")
315
+ assert FRAMEWORK_FILE_RE.search("API.py")
316
+ assert FRAMEWORK_FILE_RE.search("Routes.PY")
317
+
318
+ def test_import_regex_with_deep_modules(self):
319
+ assert TEST_IMPORT_RE.match("pytest.mark.parametrize.something")
320
+ assert TEST_IMPORT_RE.match("unittest.mock.patch.object")
321
+ assert TEST_IMPORT_RE.match("nose.tools.assert_equal")
322
+ assert not TEST_IMPORT_RE.match("requests.auth.HTTPBasicAuth")
323
+
324
+ def test_decorator_regex_whitespace_handling(self):
325
+ working_decorators = [
326
+ "pytest.fixture",
327
+ "pytest.mark",
328
+ "patch",
329
+ "responses.activate",
330
+ "freeze_time"
331
+ ]
332
+
333
+ for decorator in working_decorators:
334
+ assert TEST_DECOR_RE.match(decorator), f"Should match: {decorator}"
335
+
336
+ non_working = [
337
+ "pytest.mark.skip",
338
+ "pytest.mark.parametrize",
339
+ # may not work due to regex formatting
340
+ "patch.object"
341
+ ]
342
+
343
+ for decorator in non_working:
344
+ assert not TEST_DECOR_RE.match(decorator), f"Currently doesn't match (regex issue): {decorator}"
345
+
346
+
347
+ if __name__ == "__main__":
348
+ pytest.main([__file__, "-v"])