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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.11"
3
+ __version__ = "1.0.22"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
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, file in self.refs:
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" in class_simple_name or class_simple_name.endswith("TestCase"):
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
- p = Path(path).resolve()
126
- files = [p] if p.is_file() else list(p.glob("**/*.py"))
127
- root = p.parent if p.is_file() else p
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):return Skylos().analyze(path,conf)
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" • Use --interactive to select specific items to remove")
356
- logger.info(f" • Use --dry-run to preview changes before applying them")
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,re
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.add_ref(full)
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.add_ref(self.qual(node.id))
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 1.0.11
3
+ Version: 1.1.11
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -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