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
skylos/__init__.py
CHANGED
skylos/analyzer.py
CHANGED
|
@@ -8,10 +8,39 @@ logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(m
|
|
|
8
8
|
logger=logging.getLogger('Skylos')
|
|
9
9
|
|
|
10
10
|
AUTO_CALLED={"__init__","__enter__","__exit__"}
|
|
11
|
-
TEST_BASE_CLASSES = {"TestCase", "AsyncioTestCase", "unittest.TestCase", "unittest.AsyncioTestCase"}
|
|
12
11
|
TEST_METHOD_PATTERN = re.compile(r"^test_\w+$")
|
|
13
12
|
MAGIC_METHODS={f"__{n}__"for n in["init","new","call","getattr","getattribute","enter","exit","str","repr","hash","eq","ne","lt","gt","le","ge","iter","next","contains","len","getitem","setitem","delitem","iadd","isub","imul","itruediv","ifloordiv","imod","ipow","ilshift","irshift","iand","ixor","ior","round","format","dir","abs","complex","int","float","bool","bytes","reduce","await","aiter","anext","add","sub","mul","truediv","floordiv","mod","divmod","pow","lshift","rshift","and","or","xor","radd","rsub","rmul","rtruediv","rfloordiv","rmod","rdivmod","rpow","rlshift","rrshift","rand","ror","rxor"]}
|
|
14
13
|
|
|
14
|
+
DEFAULT_EXCLUDE_FOLDERS = {
|
|
15
|
+
"__pycache__",
|
|
16
|
+
".git",
|
|
17
|
+
".pytest_cache",
|
|
18
|
+
".mypy_cache",
|
|
19
|
+
".tox",
|
|
20
|
+
"htmlcov",
|
|
21
|
+
".coverage",
|
|
22
|
+
"build",
|
|
23
|
+
"dist",
|
|
24
|
+
"*.egg-info",
|
|
25
|
+
"venv",
|
|
26
|
+
".venv"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folders=None):
|
|
30
|
+
exclude_set = set()
|
|
31
|
+
|
|
32
|
+
if use_defaults:
|
|
33
|
+
exclude_set.update(DEFAULT_EXCLUDE_FOLDERS)
|
|
34
|
+
|
|
35
|
+
if user_exclude_folders:
|
|
36
|
+
exclude_set.update(user_exclude_folders)
|
|
37
|
+
|
|
38
|
+
if include_folders:
|
|
39
|
+
for folder in include_folders:
|
|
40
|
+
exclude_set.discard(folder)
|
|
41
|
+
|
|
42
|
+
return exclude_set
|
|
43
|
+
|
|
15
44
|
class Skylos:
|
|
16
45
|
def __init__(self):
|
|
17
46
|
self.defs={}
|
|
@@ -25,6 +54,54 @@ class Skylos:
|
|
|
25
54
|
if p[-1]=="__init__":p.pop()
|
|
26
55
|
return".".join(p)
|
|
27
56
|
|
|
57
|
+
def _should_exclude_file(self, file_path, root_path, exclude_folders):
|
|
58
|
+
if not exclude_folders:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
rel_path = file_path.relative_to(root_path)
|
|
63
|
+
except ValueError:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
path_parts = rel_path.parts
|
|
67
|
+
|
|
68
|
+
for exclude_folder in exclude_folders:
|
|
69
|
+
if "*" in exclude_folder:
|
|
70
|
+
for part in path_parts:
|
|
71
|
+
if part.endswith(exclude_folder.replace("*", "")):
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
if exclude_folder in path_parts:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def _get_python_files(self, path, exclude_folders=None):
|
|
80
|
+
p = Path(path).resolve()
|
|
81
|
+
|
|
82
|
+
if p.is_file():
|
|
83
|
+
return [p], p.parent
|
|
84
|
+
|
|
85
|
+
root = p
|
|
86
|
+
all_files = list(p.glob("**/*.py"))
|
|
87
|
+
|
|
88
|
+
if exclude_folders:
|
|
89
|
+
filtered_files = []
|
|
90
|
+
excluded_count = 0
|
|
91
|
+
|
|
92
|
+
for file_path in all_files:
|
|
93
|
+
if self._should_exclude_file(file_path, root, exclude_folders):
|
|
94
|
+
excluded_count += 1
|
|
95
|
+
continue
|
|
96
|
+
filtered_files.append(file_path)
|
|
97
|
+
|
|
98
|
+
if excluded_count > 0:
|
|
99
|
+
logger.info(f"Excluded {excluded_count} files from analysis")
|
|
100
|
+
|
|
101
|
+
return filtered_files, root
|
|
102
|
+
|
|
103
|
+
return all_files, root
|
|
104
|
+
|
|
28
105
|
def _mark_exports(self):
|
|
29
106
|
for name, d in self.defs.items():
|
|
30
107
|
if d.in_init and not d.simple_name.startswith('_'):
|
|
@@ -55,7 +132,7 @@ class Skylos:
|
|
|
55
132
|
for d in self.defs.values():
|
|
56
133
|
simple_name_lookup[d.simple_name].append(d)
|
|
57
134
|
|
|
58
|
-
for ref,
|
|
135
|
+
for ref, _ in self.refs:
|
|
59
136
|
if ref in self.defs:
|
|
60
137
|
self.defs[ref].references += 1
|
|
61
138
|
|
|
@@ -118,13 +195,30 @@ class Skylos:
|
|
|
118
195
|
if d.type == "method" and TEST_METHOD_PATTERN.match(d.simple_name):
|
|
119
196
|
class_name = d.name.rsplit(".", 1)[0]
|
|
120
197
|
class_simple_name = class_name.split(".")[-1]
|
|
121
|
-
if "Test"
|
|
198
|
+
if (class_simple_name.startswith("Test") or
|
|
199
|
+
class_simple_name.endswith("Test") or
|
|
200
|
+
class_simple_name.endswith("TestCase")):
|
|
122
201
|
d.confidence = 0
|
|
123
202
|
|
|
124
|
-
def analyze(self, path, thr=60):
|
|
125
|
-
|
|
126
|
-
files =
|
|
127
|
-
|
|
203
|
+
def analyze(self, path, thr=60, exclude_folders=None):
|
|
204
|
+
|
|
205
|
+
files, root = self._get_python_files(path, exclude_folders)
|
|
206
|
+
|
|
207
|
+
if not files:
|
|
208
|
+
logger.warning(f"No Python files found in {path}")
|
|
209
|
+
return json.dumps({
|
|
210
|
+
"unused_functions": [],
|
|
211
|
+
"unused_imports": [],
|
|
212
|
+
"unused_classes": [],
|
|
213
|
+
"unused_variables": [],
|
|
214
|
+
"unused_parameters": [],
|
|
215
|
+
"analysis_summary": {
|
|
216
|
+
"total_files": 0,
|
|
217
|
+
"excluded_folders": exclude_folders if exclude_folders else []
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
logger.info(f"Analyzing {len(files)} Python files...")
|
|
128
222
|
|
|
129
223
|
modmap = {}
|
|
130
224
|
for f in files:
|
|
@@ -156,7 +250,11 @@ class Skylos:
|
|
|
156
250
|
"unused_imports": [],
|
|
157
251
|
"unused_classes": [],
|
|
158
252
|
"unused_variables": [],
|
|
159
|
-
"unused_parameters": []
|
|
253
|
+
"unused_parameters": [],
|
|
254
|
+
"analysis_summary": {
|
|
255
|
+
"total_files": len(files),
|
|
256
|
+
"excluded_folders": exclude_folders if exclude_folders else [],
|
|
257
|
+
}
|
|
160
258
|
}
|
|
161
259
|
|
|
162
260
|
for u in unused:
|
|
@@ -188,7 +286,8 @@ def proc_file(file_or_args, mod=None):
|
|
|
188
286
|
logger.error(f"{file}: {e}")
|
|
189
287
|
return [], [], set(), set()
|
|
190
288
|
|
|
191
|
-
def analyze(path,conf=60):
|
|
289
|
+
def analyze(path,conf=60, exclude_folders=None):
|
|
290
|
+
return Skylos().analyze(path,conf, exclude_folders)
|
|
192
291
|
|
|
193
292
|
if __name__=="__main__":
|
|
194
293
|
if len(sys.argv)>1:
|
skylos/cli.py
CHANGED
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import logging
|
|
5
5
|
import ast
|
|
6
6
|
import skylos
|
|
7
|
+
from skylos.analyzer import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
|
|
7
8
|
|
|
8
9
|
try:
|
|
9
10
|
import inquirer
|
|
@@ -89,6 +90,7 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
|
|
|
89
90
|
return False
|
|
90
91
|
|
|
91
92
|
def remove_unused_function(file_path: str, function_name: str, line_number: int) -> bool:
|
|
93
|
+
# remove the entire def from the source code
|
|
92
94
|
try:
|
|
93
95
|
with open(file_path, 'r') as f:
|
|
94
96
|
content = f.read()
|
|
@@ -226,17 +228,74 @@ def main() -> None:
|
|
|
226
228
|
action="store_true",
|
|
227
229
|
help="Show what would be removed without actually modifying files"
|
|
228
230
|
)
|
|
231
|
+
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"--exclude-folder",
|
|
234
|
+
action="append",
|
|
235
|
+
dest="exclude_folders",
|
|
236
|
+
help="Exclude a folder from analysis (can be used multiple times). "
|
|
237
|
+
"By default, common folders like __pycache__, .git, venv are excluded. "
|
|
238
|
+
"Use --no-default-excludes to disable default exclusions."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--include-folder",
|
|
243
|
+
action="append",
|
|
244
|
+
dest="include_folders",
|
|
245
|
+
help="Force include a folder that would otherwise be excluded "
|
|
246
|
+
"(overrides both default and custom exclusions). "
|
|
247
|
+
"Example: --include-folder venv to scan your venv folder."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--no-default-excludes",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Don't exclude default folders (__pycache__, .git, venv, etc.). "
|
|
254
|
+
"Only exclude folders specified with --exclude-folder."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"--list-default-excludes",
|
|
259
|
+
action="store_true",
|
|
260
|
+
help="List the default excluded folders and exit."
|
|
261
|
+
)
|
|
229
262
|
|
|
230
263
|
args = parser.parse_args()
|
|
264
|
+
|
|
265
|
+
if args.list_default_excludes:
|
|
266
|
+
print("Default excluded folders:")
|
|
267
|
+
for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
|
|
268
|
+
print(f" {folder}")
|
|
269
|
+
print(f"\nTotal: {len(DEFAULT_EXCLUDE_FOLDERS)} folders")
|
|
270
|
+
print("\nUse --no-default-excludes to disable these exclusions")
|
|
271
|
+
print("Use --include-folder <folder> to force include specific folders")
|
|
272
|
+
return
|
|
273
|
+
|
|
231
274
|
logger = setup_logger(args.output)
|
|
232
275
|
|
|
233
276
|
if args.verbose:
|
|
234
277
|
logger.setLevel(logging.DEBUG)
|
|
235
278
|
logger.debug(f"Analyzing path: {args.path}")
|
|
279
|
+
if args.exclude_folders:
|
|
280
|
+
logger.debug(f"Excluding folders: {args.exclude_folders}")
|
|
281
|
+
|
|
282
|
+
use_defaults = not args.no_default_excludes
|
|
283
|
+
final_exclude_folders = parse_exclude_folders(
|
|
284
|
+
user_exclude_folders=args.exclude_folders,
|
|
285
|
+
use_defaults=use_defaults,
|
|
286
|
+
include_folders=args.include_folders
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if not args.json:
|
|
290
|
+
if final_exclude_folders:
|
|
291
|
+
logger.info(f"{Colors.YELLOW}📁 Excluding: {', '.join(sorted(final_exclude_folders))}{Colors.RESET}")
|
|
292
|
+
else:
|
|
293
|
+
logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
|
|
236
294
|
|
|
237
295
|
try:
|
|
238
|
-
result_json = skylos.analyze(args.path)
|
|
296
|
+
result_json = skylos.analyze(args.path, exclude_folders=list(final_exclude_folders))
|
|
239
297
|
result = json.loads(result_json)
|
|
298
|
+
|
|
240
299
|
except Exception as e:
|
|
241
300
|
logger.error(f"Error during analysis: {e}")
|
|
242
301
|
sys.exit(1)
|
|
@@ -259,7 +318,6 @@ def main() -> None:
|
|
|
259
318
|
logger.info(f" • Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
|
|
260
319
|
logger.info(f" • Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
|
|
261
320
|
|
|
262
|
-
|
|
263
321
|
if args.interactive and (unused_functions or unused_imports):
|
|
264
322
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
265
323
|
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
@@ -352,8 +410,9 @@ def main() -> None:
|
|
|
352
410
|
|
|
353
411
|
if unused_functions or unused_imports:
|
|
354
412
|
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
355
|
-
logger.info(f"
|
|
356
|
-
logger.info(f"
|
|
413
|
+
logger.info(f" • Use --interactive to select specific items to remove")
|
|
414
|
+
logger.info(f" • Use --dry-run to preview changes before applying them")
|
|
415
|
+
logger.info(f" • Use --exclude-folder to skip directories like node_modules, .git")
|
|
357
416
|
|
|
358
417
|
if __name__ == "__main__":
|
|
359
418
|
main()
|
skylos/visitor.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
import ast
|
|
2
|
+
import ast
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
PYTHON_BUILTINS={"print","len","str","int","float","list","dict","set","tuple","range","open","super","object","type","enumerate","zip","map","filter","sorted","reversed","sum","min","max","all","any","next","iter","repr","chr","ord","bytes","bytearray","memoryview","format","round","abs","pow","divmod","complex","hash","id","bool","callable","getattr","setattr","delattr","hasattr","isinstance","issubclass","globals","locals","vars","dir","property","classmethod","staticmethod"}
|
|
@@ -98,16 +98,13 @@ class Visitor(ast.NodeVisitor):
|
|
|
98
98
|
base = ".".join(parts[:-node.level]) + (f".{node.module}" if node.module else "")
|
|
99
99
|
|
|
100
100
|
full = f"{base}.{a.name}"
|
|
101
|
-
|
|
102
101
|
if a.asname:
|
|
103
|
-
alias_full = f"{self.mod}.{a.asname}" if self.mod else a.asname
|
|
104
|
-
self.add_def(alias_full, "import", node.lineno)
|
|
105
102
|
self.alias[a.asname] = full
|
|
106
|
-
self.
|
|
103
|
+
self.add_def(full, "import", node.lineno)
|
|
107
104
|
else:
|
|
108
105
|
self.alias[a.name] = full
|
|
109
106
|
self.add_def(full, "import", node.lineno)
|
|
110
|
-
|
|
107
|
+
|
|
111
108
|
def visit_arguments(self, args):
|
|
112
109
|
for arg in args.args:
|
|
113
110
|
self.visit_annotation(arg.annotation)
|
|
@@ -271,7 +268,8 @@ class Visitor(ast.NodeVisitor):
|
|
|
271
268
|
break
|
|
272
269
|
else:
|
|
273
270
|
# not parameter, handle normally
|
|
274
|
-
self.
|
|
271
|
+
qualified = self.qual(node.id)
|
|
272
|
+
self.add_ref(qualified)
|
|
275
273
|
if node.id in DYNAMIC_PATTERNS:
|
|
276
274
|
self.dyn.add(self.mod.split(".")[0])
|
|
277
275
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
skylos/__init__.py,sha256=ZZWhq0TZ3G-yDi9inbYvMn8OBes-pqo1aYB5EivuxFI,152
|
|
2
|
+
skylos/analyzer.py,sha256=sXLvtJ3AB946HWV9JpDiWJ4-qwcT1cKRTA3TVTx2VNU,13117
|
|
3
|
+
skylos/cli.py,sha256=7lcRFc3zailK2cPCWk6yT-EF0oeY_CmBo6TyD5m6c5Y,17355
|
|
4
|
+
skylos/visitor.py,sha256=0h07CNS6RnWi3vMjWO0sexzePRXIAfPjQib8Qxu11oY,11740
|
|
5
|
+
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
test/compare_tools.py,sha256=0g9PDeJlbst-7hOaQzrL4MiJFQKpqM8q8VeBGzpPczg,22738
|
|
7
|
+
test/conftest.py,sha256=57sTF6vLL5U0CVKwGQFJcRs6n7t1dEHIriQoSluNmAI,6418
|
|
8
|
+
test/diagnostics.py,sha256=ExuFOCVpc9BDwNYapU96vj9RXLqxji32Sv6wVF4nJYU,13802
|
|
9
|
+
test/test_analyzer.py,sha256=uHcOJjW-LDryDLFRIZgVa4TgDv2JGrUo0HPsMwh4E8I,19720
|
|
10
|
+
test/test_cli.py,sha256=rtdKzanDRJT_F92jKkCQFdhvlfwVJxfXKO8Hrbn-mIg,13180
|
|
11
|
+
test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
|
|
12
|
+
test/test_skylos.py,sha256=kz77STrS4k3Eez5RDYwGxOg2WH3e7zNZPUYEaTLbGTs,15608
|
|
13
|
+
test/test_visitor.py,sha256=sybUQtALlwl6-Nec1TalB5O6ekO9Tfia0JJZDtA7Cwc,23272
|
|
14
|
+
test/sample_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
test/sample_repo/app.py,sha256=M5XgoAn-LPz50mKAj_ZacRKf-Pg7I4HbjWP7Z9jE4a0,226
|
|
16
|
+
test/sample_repo/sample_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4Lm7m8gtCr3g,2575
|
|
18
|
+
test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
|
|
19
|
+
test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
|
|
20
|
+
test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
|
|
21
|
+
skylos-1.1.11.dist-info/METADATA,sha256=LUAqpC_E7ouGZxVtjyFa1LEXrTfV8MOyrQGRnDLnHYM,225
|
|
22
|
+
skylos-1.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
skylos-1.1.11.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
|
|
24
|
+
skylos-1.1.11.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
|
|
25
|
+
skylos-1.1.11.dist-info/RECORD,,
|
test/conftest.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
|
|
6
|
+
@pytest.fixture(scope="session")
|
|
7
|
+
def sample_project():
|
|
8
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
9
|
+
project_path = Path(temp_dir)
|
|
10
|
+
|
|
11
|
+
create_sample_project(project_path)
|
|
12
|
+
yield project_path
|
|
13
|
+
|
|
14
|
+
def create_sample_project(base_path: Path):
|
|
15
|
+
|
|
16
|
+
(base_path / "app").mkdir()
|
|
17
|
+
|
|
18
|
+
app_init = base_path / "app" / "__init__.py"
|
|
19
|
+
app_init.write_text(dedent("""
|
|
20
|
+
from .core import main_function
|
|
21
|
+
from .utils import helper_function
|
|
22
|
+
|
|
23
|
+
__all__ = ['main_function', 'helper_function']
|
|
24
|
+
"""))
|
|
25
|
+
|
|
26
|
+
core_py = base_path / "app" / "core.py"
|
|
27
|
+
core_py.write_text(dedent("""
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Dict, List # List is unused here
|
|
31
|
+
from collections import defaultdict
|
|
32
|
+
|
|
33
|
+
def main_function():
|
|
34
|
+
'''Main entry point - should not be flagged'''
|
|
35
|
+
data = defaultdict(list)
|
|
36
|
+
return process_data(data)
|
|
37
|
+
|
|
38
|
+
def process_data(data: Dict):
|
|
39
|
+
'''Used by main_function'''
|
|
40
|
+
return len(data)
|
|
41
|
+
|
|
42
|
+
def deprecated_function():
|
|
43
|
+
'''This function is never called'''
|
|
44
|
+
unused_var = "should be flagged"
|
|
45
|
+
return unused_var
|
|
46
|
+
|
|
47
|
+
def _private_helper():
|
|
48
|
+
'''Private function, might be unused'''
|
|
49
|
+
return "private"
|
|
50
|
+
"""))
|
|
51
|
+
|
|
52
|
+
utils_py = base_path / "app" / "utils.py"
|
|
53
|
+
utils_py.write_text(dedent("""
|
|
54
|
+
import json # unused
|
|
55
|
+
import re
|
|
56
|
+
|
|
57
|
+
def helper_function():
|
|
58
|
+
'''Exported function'''
|
|
59
|
+
return validate_input("test")
|
|
60
|
+
|
|
61
|
+
def validate_input(text: str):
|
|
62
|
+
'''Used by helper_function'''
|
|
63
|
+
return re.match(r"^[a-z]+$", text) is not None
|
|
64
|
+
|
|
65
|
+
def unused_utility():
|
|
66
|
+
'''Never called utility'''
|
|
67
|
+
return "utility"
|
|
68
|
+
|
|
69
|
+
class ConfigManager:
|
|
70
|
+
'''Used class'''
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.config = {}
|
|
73
|
+
|
|
74
|
+
def get(self, key):
|
|
75
|
+
return self.config.get(key)
|
|
76
|
+
|
|
77
|
+
class LegacyProcessor:
|
|
78
|
+
'''Unused class'''
|
|
79
|
+
def process(self, data):
|
|
80
|
+
return data
|
|
81
|
+
"""))
|
|
82
|
+
|
|
83
|
+
tests_dir = base_path / "tests"
|
|
84
|
+
tests_dir.mkdir()
|
|
85
|
+
|
|
86
|
+
test_core = tests_dir / "test_core.py"
|
|
87
|
+
test_core.write_text(dedent("""
|
|
88
|
+
import unittest
|
|
89
|
+
from app.core import main_function, process_data
|
|
90
|
+
|
|
91
|
+
class TestCore(unittest.TestCase):
|
|
92
|
+
def test_main_function(self):
|
|
93
|
+
result = main_function()
|
|
94
|
+
self.assertIsInstance(result, int)
|
|
95
|
+
|
|
96
|
+
def test_process_data(self):
|
|
97
|
+
data = {'key': ['value']}
|
|
98
|
+
result = process_data(data)
|
|
99
|
+
self.assertEqual(result, 1)
|
|
100
|
+
|
|
101
|
+
def test_edge_case(self):
|
|
102
|
+
'''Test methods should not be flagged'''
|
|
103
|
+
pass
|
|
104
|
+
"""))
|
|
105
|
+
|
|
106
|
+
config_dir = base_path / "config"
|
|
107
|
+
config_dir.mkdir()
|
|
108
|
+
|
|
109
|
+
config_py = config_dir / "settings.py"
|
|
110
|
+
config_py.write_text(dedent("""
|
|
111
|
+
# Configuration file with potentially unused imports
|
|
112
|
+
import os
|
|
113
|
+
import logging
|
|
114
|
+
|
|
115
|
+
DEBUG = True
|
|
116
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///app.db")
|
|
117
|
+
|
|
118
|
+
def setup_logging():
|
|
119
|
+
'''Might be called externally'''
|
|
120
|
+
logging.basicConfig(level=logging.INFO)
|
|
121
|
+
"""))
|
|
122
|
+
|
|
123
|
+
pycache_dir = base_path / "__pycache__"
|
|
124
|
+
pycache_dir.mkdir()
|
|
125
|
+
|
|
126
|
+
cache_file = pycache_dir / "cached.pyc"
|
|
127
|
+
cache_file.write_bytes(b"fake compiled python")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.fixture
|
|
131
|
+
def simple_python_file():
|
|
132
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
133
|
+
f.write(dedent("""
|
|
134
|
+
import os # unused
|
|
135
|
+
import sys
|
|
136
|
+
|
|
137
|
+
def used_function():
|
|
138
|
+
print("Hello", file=sys.stderr)
|
|
139
|
+
|
|
140
|
+
def unused_function():
|
|
141
|
+
return "never called"
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
used_function()
|
|
145
|
+
"""))
|
|
146
|
+
f.flush()
|
|
147
|
+
yield Path(f.name)
|
|
148
|
+
Path(f.name).unlink()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.fixture
|
|
152
|
+
def empty_project():
|
|
153
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
154
|
+
yield Path(temp_dir)
|
|
155
|
+
|
|
156
|
+
@pytest.fixture
|
|
157
|
+
def project_with_syntax_error():
|
|
158
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
159
|
+
project_path = Path(temp_dir)
|
|
160
|
+
|
|
161
|
+
valid_py = project_path / "valid.py"
|
|
162
|
+
valid_py.write_text("def valid_function():\n return True\n")
|
|
163
|
+
|
|
164
|
+
invalid_py = project_path / "invalid.py"
|
|
165
|
+
invalid_py.write_text("def invalid_function(\n # Missing closing parenthesis\n return False\n")
|
|
166
|
+
|
|
167
|
+
yield project_path
|
|
168
|
+
|
|
169
|
+
@pytest.fixture(autouse=True)
|
|
170
|
+
def cleanup_temp_files():
|
|
171
|
+
"""auto cleanup any temporary files after each test"""
|
|
172
|
+
yield
|
|
173
|
+
|
|
174
|
+
pytest_plugins = []
|
|
175
|
+
|
|
176
|
+
def pytest_configure(config):
|
|
177
|
+
config.addinivalue_line(
|
|
178
|
+
"markers", "integration: mark test as an integration test"
|
|
179
|
+
)
|
|
180
|
+
config.addinivalue_line(
|
|
181
|
+
"markers", "unit: mark test as a unit test"
|
|
182
|
+
)
|
|
183
|
+
config.addinivalue_line(
|
|
184
|
+
"markers", "slow: mark test as slow running"
|
|
185
|
+
)
|
|
186
|
+
config.addinivalue_line(
|
|
187
|
+
"markers", "cli: mark test as CLI interface test"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@pytest.fixture
|
|
191
|
+
def mock_git_repo():
|
|
192
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
193
|
+
repo_path = Path(temp_dir)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
import subprocess
|
|
197
|
+
subprocess.run(["git", "init"], cwd=repo_path, check=True,
|
|
198
|
+
capture_output=True)
|
|
199
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"],
|
|
200
|
+
cwd=repo_path, check=True)
|
|
201
|
+
subprocess.run(["git", "config", "user.name", "Test User"],
|
|
202
|
+
cwd=repo_path, check=True)
|
|
203
|
+
|
|
204
|
+
test_file = repo_path / "test.py"
|
|
205
|
+
test_file.write_text("def test(): pass\n")
|
|
206
|
+
subprocess.run(["git", "add", "."], cwd=repo_path, check=True)
|
|
207
|
+
subprocess.run(["git", "commit", "-m", "Initial commit"],
|
|
208
|
+
cwd=repo_path, check=True)
|
|
209
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
yield repo_path
|