multi-lang-build 0.2.5__py3-none-any.whl → 0.3.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.
Files changed (38) hide show
  1. multi_lang_build/__init__.py +4 -4
  2. multi_lang_build/cli.py +5 -2
  3. multi_lang_build/compiler/__init__.py +1 -1
  4. multi_lang_build/compiler/go_compiler.py +403 -0
  5. multi_lang_build/compiler/go_support/__init__.py +19 -0
  6. multi_lang_build/compiler/go_support/binary.py +119 -0
  7. multi_lang_build/compiler/go_support/builder.py +228 -0
  8. multi_lang_build/compiler/go_support/cleaner.py +40 -0
  9. multi_lang_build/compiler/go_support/env.py +41 -0
  10. multi_lang_build/compiler/go_support/mirror.py +111 -0
  11. multi_lang_build/compiler/go_support/module.py +80 -0
  12. multi_lang_build/compiler/go_support/tester.py +200 -0
  13. multi_lang_build/compiler/pnpm.py +61 -209
  14. multi_lang_build/compiler/pnpm_support/__init__.py +6 -0
  15. multi_lang_build/compiler/pnpm_support/executor.py +148 -0
  16. multi_lang_build/compiler/pnpm_support/project.py +53 -0
  17. multi_lang_build/compiler/python.py +65 -222
  18. multi_lang_build/compiler/python_support/__init__.py +13 -0
  19. multi_lang_build/compiler/python_support/cleaner.py +56 -0
  20. multi_lang_build/compiler/python_support/cli.py +46 -0
  21. multi_lang_build/compiler/python_support/detector.py +64 -0
  22. multi_lang_build/compiler/python_support/installer.py +162 -0
  23. multi_lang_build/compiler/python_support/venv.py +63 -0
  24. multi_lang_build/ide_register.py +97 -0
  25. multi_lang_build/mirror/config.py +19 -0
  26. multi_lang_build/register/support/__init__.py +13 -0
  27. multi_lang_build/register/support/claude.py +94 -0
  28. multi_lang_build/register/support/codebuddy.py +100 -0
  29. multi_lang_build/register/support/opencode.py +62 -0
  30. multi_lang_build/register/support/trae.py +72 -0
  31. {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/METADATA +41 -5
  32. multi_lang_build-0.3.0.dist-info/RECORD +40 -0
  33. multi_lang_build/compiler/go.py +0 -468
  34. multi_lang_build/register.py +0 -412
  35. multi_lang_build-0.2.5.dist-info/RECORD +0 -18
  36. {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/WHEEL +0 -0
  37. {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/entry_points.txt +0 -0
  38. {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,35 +5,33 @@ from typing import Final
5
5
  import shutil
6
6
  import subprocess
7
7
  import sys
8
- import venv
9
- import json
10
- import time
11
8
 
12
9
  from multi_lang_build.compiler.base import CompilerBase, BuildResult, CompilerInfo
13
10
  from multi_lang_build.mirror.config import (
14
- get_mirror_config,
15
11
  apply_mirror_environment,
16
- PYPI_MIRROR_TSINGHUA,
17
- PYPI_MIRROR_ALIYUN,
18
- PYPI_MIRROR_DOUBAN,
19
- PYPI_MIRROR_HUAWEI,
20
12
  DEFAULT_PIP_MIRROR,
21
13
  )
14
+ from multi_lang_build.compiler.python_support import (
15
+ BuildSystemDetector,
16
+ DependencyInstaller,
17
+ VenvManager,
18
+ PythonCleaner,
19
+ )
22
20
 
23
21
 
24
22
  class PythonCompiler(CompilerBase):
25
23
  """Compiler for Python projects with pip and poetry support."""
26
-
24
+
27
25
  NAME: Final[str] = "python"
28
26
  DEFAULT_MIRROR: Final[str] = DEFAULT_PIP_MIRROR
29
-
27
+
30
28
  def __init__(
31
- self,
29
+ self,
32
30
  python_path: str | None = None,
33
31
  mirror: str | None = None,
34
32
  ) -> None:
35
33
  """Initialize the Python compiler.
36
-
34
+
37
35
  Args:
38
36
  python_path: Optional path to python executable. If None, uses sys.executable.
39
37
  mirror: Mirror configuration name (e.g., "pip", "pip_aliyun", "pip_douban", "pip_huawei").
@@ -42,20 +40,20 @@ class PythonCompiler(CompilerBase):
42
40
  self._python_path = python_path
43
41
  self._mirror = mirror
44
42
  self._version_cache: str | None = None
45
-
43
+
46
44
  @property
47
45
  def name(self) -> str:
48
46
  """Get the compiler name."""
49
47
  return self.NAME
50
-
48
+
51
49
  @property
52
50
  def version(self) -> str:
53
51
  """Get the Python version."""
54
52
  if self._version_cache:
55
53
  return self._version_cache
56
-
54
+
57
55
  python_executable = self._get_executable_path()
58
-
56
+
59
57
  try:
60
58
  result = subprocess.run(
61
59
  [python_executable, "--version"],
@@ -67,19 +65,19 @@ class PythonCompiler(CompilerBase):
67
65
  self._version_cache = output.replace("Python ", "")
68
66
  except Exception:
69
67
  self._version_cache = sys.version.split()[0]
70
-
68
+
71
69
  return self._version_cache
72
-
70
+
73
71
  @property
74
72
  def supported_mirrors(self) -> list[str]:
75
73
  """Get list of supported mirror configurations."""
76
74
  return ["pip", "pip_aliyun", "pip_douban", "pip_huawei"]
77
-
75
+
78
76
  @property
79
77
  def current_mirror(self) -> str:
80
78
  """Get the current mirror configuration name."""
81
79
  return self._mirror or "pip"
82
-
80
+
83
81
  def get_info(self) -> CompilerInfo:
84
82
  """Get compiler information."""
85
83
  return {
@@ -89,15 +87,15 @@ class PythonCompiler(CompilerBase):
89
87
  "default_mirror": self.DEFAULT_MIRROR,
90
88
  "executable": self._get_executable_path(),
91
89
  }
92
-
90
+
93
91
  def set_mirror(self, mirror: str) -> None:
94
92
  """Set the mirror configuration.
95
-
93
+
96
94
  Args:
97
95
  mirror: Mirror configuration name (e.g., "pip", "pip_aliyun", "pip_douban", "pip_huawei")
98
96
  """
99
97
  self._mirror = mirror
100
-
98
+
101
99
  def build(
102
100
  self,
103
101
  source_dir: Path,
@@ -122,20 +120,20 @@ class PythonCompiler(CompilerBase):
122
120
  BuildResult containing success status and output information.
123
121
  """
124
122
  python_executable = self._get_executable_path()
125
-
123
+
126
124
  # Validate directories
127
125
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
128
126
  output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
129
-
127
+
130
128
  # Prepare environment
131
129
  env = environment.copy() if environment else {}
132
-
130
+
133
131
  if mirror_enabled:
134
132
  env = apply_mirror_environment("python", env)
135
133
  env = apply_mirror_environment("pip", env)
136
-
134
+
137
135
  # Determine build system
138
- build_system = self._detect_build_system(source_dir)
136
+ build_system = BuildSystemDetector.detect(source_dir)
139
137
 
140
138
  if build_system == "poetry":
141
139
  return self._build_with_poetry(
@@ -159,42 +157,7 @@ class PythonCompiler(CompilerBase):
159
157
  output_path=None,
160
158
  duration_seconds=0.0,
161
159
  )
162
-
163
- def _detect_build_system(self, source_dir: Path) -> str:
164
- """Detect the build system used by the project.
165
-
166
- Args:
167
- source_dir: Source directory to check
168
-
169
- Returns:
170
- Build system name: "poetry", "setuptools", "pdm", or "none"
171
- """
172
- pyproject = source_dir / "pyproject.toml"
173
- setup_py = source_dir / "setup.py"
174
- setup_cfg = source_dir / "setup.cfg"
175
- pdm_pyproject = source_dir / "pyproject.toml"
176
-
177
- # Check for poetry
178
- if pyproject.exists():
179
- try:
180
- content = pyproject.read_text()
181
- if "[tool.poetry]" in content:
182
- return "poetry"
183
- if "[tool.pdm]" in content:
184
- return "pdm"
185
- except Exception:
186
- pass
187
-
188
- # Check for setuptools
189
- if setup_py.exists() or setup_cfg.exists():
190
- return "setuptools"
191
-
192
- # Check pdm
193
- if pdm_pyproject.exists():
194
- return "pdm"
195
-
196
- return "none"
197
-
160
+
198
161
  def _build_with_poetry(
199
162
  self,
200
163
  python_executable: str,
@@ -250,10 +213,10 @@ class PythonCompiler(CompilerBase):
250
213
  else:
251
214
  # Legacy setup.py
252
215
  build_cmd = [python_executable, "setup.py", "bdist_wheel", "--dist-dir", str(output_dir)]
253
-
216
+
254
217
  if extra_args:
255
218
  build_cmd.extend(extra_args)
256
-
219
+
257
220
  return self._run_build(
258
221
  build_cmd,
259
222
  source_dir,
@@ -285,7 +248,7 @@ class PythonCompiler(CompilerBase):
285
248
  environment=env,
286
249
  stream_output=stream_output,
287
250
  )
288
-
251
+
289
252
  def install_dependencies(
290
253
  self,
291
254
  source_dir: Path,
@@ -296,42 +259,37 @@ class PythonCompiler(CompilerBase):
296
259
  poetry: bool = False,
297
260
  ) -> BuildResult:
298
261
  """Install Python dependencies.
299
-
262
+
300
263
  Args:
301
264
  source_dir: Source code directory
302
265
  environment: Additional environment variables
303
266
  mirror_enabled: Whether to use PyPI mirror
304
267
  dev: Include development dependencies
305
268
  poetry: Force using poetry
306
-
269
+
307
270
  Returns:
308
271
  BuildResult containing success status and output information.
309
272
  """
310
273
  python_executable = self._get_executable_path()
311
-
274
+
312
275
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
313
-
276
+
314
277
  env = environment.copy() if environment else {}
315
-
278
+
316
279
  if mirror_enabled:
317
280
  env = apply_mirror_environment("pip", env)
318
-
281
+
319
282
  # Check for poetry first
320
- if poetry or (source_dir / "pyproject.toml").exists() and "poetry" in (source_dir / "pyproject.toml").read_text():
321
- cmd = [python_executable, "-m", "poetry", "install"]
322
- if dev:
323
- cmd.append("--with=dev")
324
- return self._run_build(cmd, source_dir, source_dir, environment=env)
325
-
283
+ if poetry or BuildSystemDetector.is_poetry(source_dir):
284
+ return DependencyInstaller.install_poetry(
285
+ python_executable, source_dir, env, dev, True
286
+ )
287
+
326
288
  # Use pip
327
- cmd = [python_executable, "-m", "pip", "install", "-r", str(source_dir / "requirements.txt")]
328
-
329
- if dev:
330
- cmd.append("-r")
331
- cmd.append(str(source_dir / "requirements-dev.txt"))
332
-
333
- return self._run_build(cmd, source_dir, source_dir, environment=env)
334
-
289
+ return DependencyInstaller.install_pip(
290
+ python_executable, source_dir, env, dev, True
291
+ )
292
+
335
293
  def create_venv(
336
294
  self,
337
295
  directory: Path,
@@ -340,52 +298,17 @@ class PythonCompiler(CompilerBase):
340
298
  mirror_enabled: bool = True,
341
299
  ) -> BuildResult:
342
300
  """Create a virtual environment.
343
-
301
+
344
302
  Args:
345
303
  directory: Directory to create the venv in
346
304
  python_path: Python interpreter to use
347
305
  mirror_enabled: Whether to configure pip mirror
348
-
306
+
349
307
  Returns:
350
308
  BuildResult containing success status and output information.
351
309
  """
352
- env_builder = venv.EnvBuilder(with_pip=True, clear=True)
353
-
354
- start_time = time.perf_counter() # noqa: F821
355
-
356
- try:
357
- env_builder.create(directory)
358
-
359
- # Configure pip mirror if enabled
360
- if mirror_enabled:
361
- pip_config_dir = directory / "pip.conf"
362
- pip_config_dir.write_text(
363
- "[global]\n"
364
- "index-url = https://pypi.tuna.tsinghua.edu.cn/simple\n"
365
- "trusted-host = pypi.tuna.tsinghua.edu.cn\n"
366
- )
367
-
368
- duration = time.perf_counter() - start_time # noqa: F821
369
-
370
- return BuildResult(
371
- success=True,
372
- return_code=0,
373
- stdout=f"Virtual environment created at {directory}",
374
- stderr="",
375
- output_path=directory,
376
- duration_seconds=duration,
377
- )
378
- except Exception as e:
379
- duration = time.perf_counter() - start_time # noqa: F821
380
- return BuildResult(
381
- success=False,
382
- return_code=-1,
383
- stdout="",
384
- stderr=f"Failed to create virtual environment: {str(e)}",
385
- output_path=None,
386
- duration_seconds=duration,
387
- )
388
-
310
+ return VenvManager.create(directory, python_path, mirror_enabled)
311
+
389
312
  def run_script(
390
313
  self,
391
314
  source_dir: Path,
@@ -395,145 +318,65 @@ class PythonCompiler(CompilerBase):
395
318
  mirror_enabled: bool = True,
396
319
  ) -> BuildResult:
397
320
  """Run a Python script or module.
398
-
321
+
399
322
  Args:
400
323
  source_dir: Source code directory
401
324
  script_name: Script or module name to run
402
325
  environment: Additional environment variables
403
326
  mirror_enabled: Whether to use PyPI mirror
404
-
327
+
405
328
  Returns:
406
329
  BuildResult containing success status and output information.
407
330
  """
408
331
  python_executable = self._get_executable_path()
409
-
332
+
410
333
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
411
-
334
+
412
335
  env = environment.copy() if environment else {}
413
-
336
+
414
337
  if mirror_enabled:
415
338
  env = apply_mirror_environment("pip", env)
416
-
339
+
417
340
  return self._run_build(
418
341
  [python_executable, "-m", script_name],
419
342
  source_dir,
420
343
  source_dir,
421
344
  environment=env,
422
345
  )
423
-
346
+
424
347
  def clean(self, directory: Path) -> bool:
425
348
  """Clean Python build artifacts in the specified directory.
426
-
349
+
427
350
  Args:
428
351
  directory: Directory to clean
429
-
352
+
430
353
  Returns:
431
354
  True if successful, False otherwise.
432
355
  """
433
- import shutil
434
-
435
- try:
436
- directory = self._validate_directory(directory, create_if_not_exists=False)
437
-
438
- # Remove __pycache__ directories
439
- for pycache in directory.rglob("__pycache__"):
440
- shutil.rmtree(pycache)
441
-
442
- # Remove .pyc files
443
- for pyc in directory.rglob("*.pyc"):
444
- pyc.unlink()
445
-
446
- # Remove .pytest_cache
447
- pytest_cache = directory / ".pytest_cache"
448
- if pytest_cache.exists():
449
- shutil.rmtree(pytest_cache)
450
-
451
- # Remove .tox
452
- tox_dir = directory / ".tox"
453
- if tox_dir.exists():
454
- shutil.rmtree(tox_dir)
455
-
456
- # Remove dist directory
457
- dist_dir = directory / "dist"
458
- if dist_dir.exists():
459
- shutil.rmtree(dist_dir)
460
-
461
- # Remove build directory
462
- build_dir = directory / "build"
463
- if build_dir.exists():
464
- shutil.rmtree(build_dir)
465
-
466
- # Remove *.egg-info directories
467
- for egg_info in directory.rglob("*.egg-info"):
468
- shutil.rmtree(egg_info)
469
-
470
- return True
471
-
472
- except Exception:
473
- return False
474
-
356
+ return PythonCleaner.clean(directory)
357
+
475
358
  def _get_executable_path(self) -> str:
476
359
  """Get the python executable path."""
477
360
  if self._python_path:
478
361
  return self._python_path
479
-
362
+
480
363
  python_path = shutil.which("python3") or shutil.which("python")
481
364
  if python_path is None:
482
365
  return sys.executable
483
-
366
+
484
367
  return python_path
485
-
368
+
486
369
  @staticmethod
487
370
  def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PythonCompiler":
488
371
  """Factory method to create a PythonCompiler instance.
489
-
372
+
490
373
  Args:
491
374
  source_dir: Source directory for the project
492
375
  mirror_enabled: Whether to enable mirror acceleration by default
493
-
376
+
494
377
  Returns:
495
378
  Configured PythonCompiler instance
496
379
  """
497
380
  compiler = PythonCompiler()
498
381
  return compiler
499
382
 
500
-
501
- def main() -> None:
502
- """Python compiler CLI entry point."""
503
- import argparse
504
- import sys
505
-
506
- parser = argparse.ArgumentParser(description="Python Build Compiler")
507
- parser.add_argument("source_dir", type=Path, help="Source directory")
508
- parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
509
- parser.add_argument("--mirror", action="store_true", default=True, help="Enable PyPI mirror")
510
- parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable PyPI mirror")
511
- parser.add_argument("--install", action="store_true", help="Install dependencies only")
512
- parser.add_argument("--dev", action="store_true", help="Include dev dependencies")
513
- parser.add_argument("--poetry", action="store_true", help="Force using poetry")
514
- parser.add_argument("--create-venv", type=Path, help="Create virtual environment at path")
515
-
516
- args = parser.parse_args()
517
-
518
- compiler = PythonCompiler()
519
-
520
- if args.create_venv:
521
- result = compiler.create_venv(
522
- args.create_venv,
523
- mirror_enabled=args.mirror,
524
- )
525
- elif args.install:
526
- result = compiler.install_dependencies(
527
- args.source_dir,
528
- mirror_enabled=args.mirror,
529
- dev=args.dev,
530
- poetry=args.poetry,
531
- )
532
- else:
533
- result = compiler.build(
534
- args.source_dir,
535
- args.output,
536
- mirror_enabled=args.mirror,
537
- )
538
-
539
- sys.exit(0 if result["success"] else 1)
@@ -0,0 +1,13 @@
1
+ """Python compiler support modules."""
2
+
3
+ from multi_lang_build.compiler.python_support.detector import BuildSystemDetector
4
+ from multi_lang_build.compiler.python_support.installer import DependencyInstaller
5
+ from multi_lang_build.compiler.python_support.venv import VenvManager
6
+ from multi_lang_build.compiler.python_support.cleaner import PythonCleaner
7
+
8
+ __all__ = [
9
+ "BuildSystemDetector",
10
+ "DependencyInstaller",
11
+ "VenvManager",
12
+ "PythonCleaner",
13
+ ]
@@ -0,0 +1,56 @@
1
+ """Python build artifact cleaning utilities."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+
7
+ class PythonCleaner:
8
+ """Clean Python build artifacts."""
9
+
10
+ @staticmethod
11
+ def clean(directory: Path) -> bool:
12
+ """Clean Python build artifacts in the specified directory.
13
+
14
+ Args:
15
+ directory: Directory to clean
16
+
17
+ Returns:
18
+ True if successful, False otherwise.
19
+ """
20
+ try:
21
+ # Remove __pycache__ directories
22
+ for pycache in directory.rglob("__pycache__"):
23
+ shutil.rmtree(pycache)
24
+
25
+ # Remove .pyc files
26
+ for pyc in directory.rglob("*.pyc"):
27
+ pyc.unlink()
28
+
29
+ # Remove .pytest_cache
30
+ pytest_cache = directory / ".pytest_cache"
31
+ if pytest_cache.exists():
32
+ shutil.rmtree(pytest_cache)
33
+
34
+ # Remove .tox
35
+ tox_dir = directory / ".tox"
36
+ if tox_dir.exists():
37
+ shutil.rmtree(tox_dir)
38
+
39
+ # Remove dist directory
40
+ dist_dir = directory / "dist"
41
+ if dist_dir.exists():
42
+ shutil.rmtree(dist_dir)
43
+
44
+ # Remove build directory
45
+ build_dir = directory / "build"
46
+ if build_dir.exists():
47
+ shutil.rmtree(build_dir)
48
+
49
+ # Remove *.egg-info directories
50
+ for egg_info in directory.rglob("*.egg-info"):
51
+ shutil.rmtree(egg_info)
52
+
53
+ return True
54
+
55
+ except Exception:
56
+ return False
@@ -0,0 +1,46 @@
1
+ """Python compiler CLI entry point."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def main() -> None:
7
+ """Python compiler CLI entry point."""
8
+ import argparse
9
+ import sys
10
+
11
+ parser = argparse.ArgumentParser(description="Python Build Compiler")
12
+ parser.add_argument("source_dir", type=Path, help="Source directory")
13
+ parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
14
+ parser.add_argument("--mirror", action="store_true", default=True, help="Enable PyPI mirror")
15
+ parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable PyPI mirror")
16
+ parser.add_argument("--install", action="store_true", help="Install dependencies only")
17
+ parser.add_argument("--dev", action="store_true", help="Include dev dependencies")
18
+ parser.add_argument("--poetry", action="store_true", help="Force using poetry")
19
+ parser.add_argument("--create-venv", type=Path, help="Create virtual environment at path")
20
+
21
+ args = parser.parse_args()
22
+
23
+ from multi_lang_build.compiler.python import PythonCompiler
24
+
25
+ compiler = PythonCompiler()
26
+
27
+ if args.create_venv:
28
+ result = compiler.create_venv(
29
+ args.create_venv,
30
+ mirror_enabled=args.mirror,
31
+ )
32
+ elif args.install:
33
+ result = compiler.install_dependencies(
34
+ args.source_dir,
35
+ mirror_enabled=args.mirror,
36
+ dev=args.dev,
37
+ poetry=args.poetry,
38
+ )
39
+ else:
40
+ result = compiler.build(
41
+ args.source_dir,
42
+ args.output,
43
+ mirror_enabled=args.mirror,
44
+ )
45
+
46
+ sys.exit(0 if result["success"] else 1)
@@ -0,0 +1,64 @@
1
+ """Python build system detection."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class BuildSystemDetector:
7
+ """Detect Python build systems (Poetry, setuptools, PDM)."""
8
+
9
+ @staticmethod
10
+ def detect(source_dir: Path) -> str:
11
+ """Detect the build system used by the project.
12
+
13
+ Args:
14
+ source_dir: Source directory to check
15
+
16
+ Returns:
17
+ Build system name: "poetry", "setuptools", "pdm", or "none"
18
+ """
19
+ pyproject = source_dir / "pyproject.toml"
20
+ setup_py = source_dir / "setup.py"
21
+ setup_cfg = source_dir / "setup.cfg"
22
+
23
+ # Check for poetry
24
+ if pyproject.exists():
25
+ try:
26
+ content = pyproject.read_text()
27
+ if "[tool.poetry]" in content:
28
+ return "poetry"
29
+ if "[tool.pdm]" in content:
30
+ return "pdm"
31
+ except Exception:
32
+ pass
33
+
34
+ # Check for setuptools
35
+ if setup_py.exists() or setup_cfg.exists():
36
+ return "setuptools"
37
+
38
+ # Check pdm
39
+ if pyproject.exists():
40
+ return "pdm"
41
+
42
+ return "none"
43
+
44
+ @staticmethod
45
+ def is_poetry(source_dir: Path) -> bool:
46
+ """Check if project uses Poetry."""
47
+ pyproject = source_dir / "pyproject.toml"
48
+ if not pyproject.exists():
49
+ return False
50
+ try:
51
+ return "[tool.poetry]" in pyproject.read_text()
52
+ except Exception:
53
+ return False
54
+
55
+ @staticmethod
56
+ def is_pdm(source_dir: Path) -> bool:
57
+ """Check if project uses PDM."""
58
+ pyproject = source_dir / "pyproject.toml"
59
+ if not pyproject.exists():
60
+ return False
61
+ try:
62
+ return "[tool.pdm]" in pyproject.read_text()
63
+ except Exception:
64
+ return False