simfix 0.1.3__tar.gz → 0.1.5__tar.gz

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 (46) hide show
  1. {simfix-0.1.3/simfix.egg-info → simfix-0.1.5}/PKG-INFO +2 -1
  2. {simfix-0.1.3 → simfix-0.1.5}/pyproject.toml +2 -1
  3. {simfix-0.1.3 → simfix-0.1.5}/simfix/__init__.py +1 -1
  4. {simfix-0.1.3 → simfix-0.1.5}/simfix/analyzer.py +45 -17
  5. {simfix-0.1.3 → simfix-0.1.5}/simfix/cli.py +19 -0
  6. simfix-0.1.5/simfix/commands.py +130 -0
  7. simfix-0.1.5/simfix/cuda.py +133 -0
  8. simfix-0.1.5/simfix/dependency_discovery.py +139 -0
  9. simfix-0.1.5/simfix/install_commands.py +163 -0
  10. simfix-0.1.5/simfix/recommendation_types.py +16 -0
  11. simfix-0.1.5/simfix/recommendations.py +284 -0
  12. simfix-0.1.5/simfix/ros_environment.py +135 -0
  13. simfix-0.1.5/simfix/system_capabilities.py +46 -0
  14. simfix-0.1.5/simfix/vendor_dependencies.py +134 -0
  15. {simfix-0.1.3 → simfix-0.1.5/simfix.egg-info}/PKG-INFO +2 -1
  16. {simfix-0.1.3 → simfix-0.1.5}/simfix.egg-info/SOURCES.txt +7 -0
  17. {simfix-0.1.3 → simfix-0.1.5}/simfix.egg-info/requires.txt +3 -0
  18. {simfix-0.1.3 → simfix-0.1.5}/tests/test_basic.py +270 -5
  19. simfix-0.1.3/simfix/commands.py +0 -91
  20. simfix-0.1.3/simfix/recommendations.py +0 -131
  21. {simfix-0.1.3 → simfix-0.1.5}/LICENSE +0 -0
  22. {simfix-0.1.3 → simfix-0.1.5}/README.md +0 -0
  23. {simfix-0.1.3 → simfix-0.1.5}/setup.cfg +0 -0
  24. {simfix-0.1.3 → simfix-0.1.5}/simfix/cmake.py +0 -0
  25. {simfix-0.1.3 → simfix-0.1.5}/simfix/compatibility.py +0 -0
  26. {simfix-0.1.3 → simfix-0.1.5}/simfix/conda_environment.py +0 -0
  27. {simfix-0.1.3 → simfix-0.1.5}/simfix/conda_fixer.py +0 -0
  28. {simfix-0.1.3 → simfix-0.1.5}/simfix/cuda_docker.py +0 -0
  29. {simfix-0.1.3 → simfix-0.1.5}/simfix/docker_runner.py +0 -0
  30. {simfix-0.1.3 → simfix-0.1.5}/simfix/dockerfile.py +0 -0
  31. {simfix-0.1.3 → simfix-0.1.5}/simfix/fixer.py +0 -0
  32. {simfix-0.1.3 → simfix-0.1.5}/simfix/git_assets.py +0 -0
  33. {simfix-0.1.3 → simfix-0.1.5}/simfix/planner.py +0 -0
  34. {simfix-0.1.3 → simfix-0.1.5}/simfix/pypi.py +0 -0
  35. {simfix-0.1.3 → simfix-0.1.5}/simfix/pyproject.py +0 -0
  36. {simfix-0.1.3 → simfix-0.1.5}/simfix/python_requirements.py +0 -0
  37. {simfix-0.1.3 → simfix-0.1.5}/simfix/repo.py +0 -0
  38. {simfix-0.1.3 → simfix-0.1.5}/simfix/report.py +0 -0
  39. {simfix-0.1.3 → simfix-0.1.5}/simfix/ros_docker.py +0 -0
  40. {simfix-0.1.3 → simfix-0.1.5}/simfix/ros_package.py +0 -0
  41. {simfix-0.1.3 → simfix-0.1.5}/simfix/setup_py.py +0 -0
  42. {simfix-0.1.3 → simfix-0.1.5}/simfix/system.py +0 -0
  43. {simfix-0.1.3 → simfix-0.1.5}/simfix/system_docker.py +0 -0
  44. {simfix-0.1.3 → simfix-0.1.5}/simfix.egg-info/dependency_links.txt +0 -0
  45. {simfix-0.1.3 → simfix-0.1.5}/simfix.egg-info/entry_points.txt +0 -0
  46. {simfix-0.1.3 → simfix-0.1.5}/simfix.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simfix
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A dependency checker and installation assistant for simulator repositories.
5
5
  Author-email: Habib ur Rehmaan <h.rehmaan96@gmail.com>
6
6
  License-Expression: MIT
@@ -26,6 +26,7 @@ Requires-Dist: requests
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: pyyaml
28
28
  Requires-Dist: uv
29
+ Requires-Dist: tomli; python_version < "3.11"
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest; extra == "dev"
31
32
  Requires-Dist: pre-commit; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simfix"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "A dependency checker and installation assistant for simulator repositories."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -39,6 +39,7 @@ dependencies = [
39
39
  "packaging",
40
40
  "pyyaml",
41
41
  "uv",
42
+ "tomli; python_version < '3.11'",
42
43
  ]
43
44
 
44
45
  [project.optional-dependencies]
@@ -1,3 +1,3 @@
1
1
  """SimFix package."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -10,6 +10,7 @@ from simfix.pypi import normalize_requirement_name
10
10
  from simfix.pyproject import PyProjectInfo, parse_pyproject
11
11
  from simfix.python_requirements import parse_requirements_file
12
12
  from simfix.ros_package import ROSPackageInfo, parse_ros_package
13
+ from simfix.dependency_discovery import discover_dependency_files
13
14
  from simfix.setup_py import parse_setup_py_dependencies
14
15
 
15
16
 
@@ -89,33 +90,60 @@ def analyze_repo(repo_path: str | Path) -> RepoAnalysis:
89
90
  if not path.is_dir():
90
91
  raise NotADirectoryError(f"Repository path is not a directory: {path}")
91
92
 
92
- requirements_path = path / "requirements.txt"
93
- environment_path = path / "environment.yml"
94
- if not environment_path.exists():
95
- environment_path = path / "environment.yaml"
93
+ discovered_files = discover_dependency_files(path)
94
+
95
+ requirements_path = (
96
+ discovered_files.requirements_txt[0]
97
+ if discovered_files.requirements_txt
98
+ else path / "requirements.txt"
99
+ )
100
+ environment_path = (
101
+ discovered_files.environment_files[0]
102
+ if discovered_files.environment_files
103
+ else path / "environment.yml"
104
+ )
105
+ dockerfile_path = (
106
+ discovered_files.dockerfiles[0]
107
+ if discovered_files.dockerfiles
108
+ else path / "Dockerfile"
109
+ )
110
+ package_xml_path = (
111
+ discovered_files.package_xml_files[0]
112
+ if discovered_files.package_xml_files
113
+ else path / "package.xml"
114
+ )
115
+ cmake_path = (
116
+ discovered_files.cmake_lists_files[0]
117
+ if discovered_files.cmake_lists_files
118
+ else path / "CMakeLists.txt"
119
+ )
120
+ pyproject_path = (
121
+ discovered_files.pyproject_toml[0]
122
+ if discovered_files.pyproject_toml
123
+ else path / "pyproject.toml"
124
+ )
125
+ setup_py_path = (
126
+ discovered_files.setup_py_files[0]
127
+ if discovered_files.setup_py_files
128
+ else path / "setup.py"
129
+ )
96
130
 
97
- dockerfile_path = path / "Dockerfile"
98
- package_xml_path = path / "package.xml"
99
- cmake_path = path / "CMakeLists.txt"
100
- pyproject_path = path / "pyproject.toml"
101
- setup_py_path = path / "setup.py"
102
131
  setup_py_dependencies = parse_setup_py_dependencies(setup_py_path)
103
132
 
104
133
  return RepoAnalysis(
105
134
  repo_path=path,
106
- has_requirements_txt=requirements_path.exists(),
107
- has_environment_yml=(path / "environment.yml").exists()
108
- or (path / "environment.yaml").exists(),
135
+ has_requirements_txt=discovered_files.has_requirements_txt,
136
+ has_environment_yml=discovered_files.has_environment_file,
109
137
  python_requirements=parse_requirements_file(requirements_path),
110
138
  conda_environment=parse_conda_environment(environment_path),
111
- has_dockerfile=dockerfile_path.exists(),
139
+ has_dockerfile=discovered_files.has_dockerfile,
112
140
  dockerfile_info=parse_dockerfile(dockerfile_path),
113
- has_package_xml=package_xml_path.exists(),
141
+ has_package_xml=discovered_files.has_ros_package,
114
142
  ros_package_info=parse_ros_package(package_xml_path),
115
- has_cmake=cmake_path.exists(),
143
+ has_cmake=discovered_files.has_cmake_lists,
116
144
  cmake_info=parse_cmake_file(cmake_path),
117
- has_pyproject_toml=pyproject_path.exists(),
145
+ has_pyproject_toml=discovered_files.has_pyproject_toml,
118
146
  pyproject_info=parse_pyproject(pyproject_path),
119
- has_setup_py=setup_py_path.exists(),
147
+ has_setup_py=discovered_files.has_setup_py,
120
148
  setup_py_dependencies=setup_py_dependencies,
121
149
  )
@@ -15,8 +15,11 @@ from simfix.planner import create_install_plan
15
15
  from simfix.pypi import check_pypi_packages
16
16
  from simfix.repo import clone_repo, is_git_url
17
17
  from simfix.report import generate_markdown_report, write_markdown_report
18
+ from simfix.system_capabilities import detect_system_capabilities
18
19
  from simfix.system import get_system_info
19
20
  from simfix.recommendations import generate_recommendations
21
+ from simfix.cuda import detect_cuda_version_info
22
+ from simfix.ros_environment import detect_ros_environment_info
20
23
 
21
24
  console = Console()
22
25
 
@@ -120,6 +123,9 @@ def recommendations(repo: str) -> None:
120
123
  repo_recommendations = generate_recommendations(
121
124
  dependencies=analysis.all_python_dependencies,
122
125
  detected_ecosystems=analysis.detected_ecosystems,
126
+ system_capabilities=detect_system_capabilities(),
127
+ cuda_version_info=detect_cuda_version_info(repo_path),
128
+ ros_environment_info=detect_ros_environment_info(repo_path),
123
129
  )
124
130
 
125
131
  console.print("[bold]SimFix Recommendations[/bold]")
@@ -208,6 +214,7 @@ def doctor(
208
214
  console.print(f"[bold]Detected ecosystem(s):[/bold] {ecosystems}")
209
215
 
210
216
  python_dependencies = analysis.all_python_dependencies
217
+ pypi_results = []
211
218
 
212
219
  if python_dependencies:
213
220
  deps_table = Table(title="Python packages")
@@ -218,6 +225,15 @@ def doctor(
218
225
 
219
226
  console.print(deps_table)
220
227
 
228
+ pypi_table = Table(title="PyPI check")
229
+ pypi_table.add_column("Package", style="cyan")
230
+ pypi_table.add_column("Status")
231
+
232
+ for result in pypi_results:
233
+ status = "found" if result.exists else "not found"
234
+ pypi_table.add_row(result.name, status)
235
+
236
+ console.print(pypi_table)
221
237
  pypi_results = check_pypi_packages(python_dependencies)
222
238
 
223
239
  pypi_table = Table(title="PyPI check")
@@ -405,6 +421,9 @@ def doctor(
405
421
  repo_recommendations = generate_recommendations(
406
422
  dependencies=analysis.all_python_dependencies,
407
423
  detected_ecosystems=analysis.detected_ecosystems,
424
+ system_capabilities=detect_system_capabilities(),
425
+ cuda_version_info=detect_cuda_version_info(repo_path),
426
+ ros_environment_info=detect_ros_environment_info(repo_path),
408
427
  )
409
428
 
410
429
  if repo_recommendations:
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from simfix.analyzer import RepoAnalysis
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CommandPlan:
10
+ """Suggested shell commands for installing a repository."""
11
+
12
+ title: str
13
+ commands: list[str]
14
+
15
+
16
+ def create_command_plan(analysis: RepoAnalysis) -> CommandPlan:
17
+ """Create suggested installation commands from repository analysis."""
18
+ ecosystems = analysis.detected_ecosystems
19
+ repo_name = analysis.repo_path.name.replace("_", "-").lower()
20
+ commands: list[str] = []
21
+
22
+ if "docker" in ecosystems:
23
+ commands.append(f"docker build -t {repo_name} .")
24
+
25
+ run_helper = analysis.repo_path / "run_simfix_docker.sh"
26
+ if run_helper.exists():
27
+ commands.append("./run_simfix_docker.sh")
28
+ else:
29
+ commands.append(f"docker run --rm -it {repo_name}")
30
+
31
+ if "ros" in ecosystems:
32
+ build_system = analysis.ros_package_info.build_system.lower()
33
+
34
+ commands.extend(
35
+ [
36
+ "rosdep update",
37
+ "rosdep install --from-paths . --ignore-src -r -y",
38
+ ]
39
+ )
40
+
41
+ if build_system == "catkin":
42
+ commands.extend(
43
+ [
44
+ "catkin build",
45
+ "source devel/setup.bash",
46
+ ]
47
+ )
48
+ elif build_system in {"ament", "ament_cmake", "ament_python"}:
49
+ commands.extend(
50
+ [
51
+ "colcon build",
52
+ "source install/setup.bash",
53
+ ]
54
+ )
55
+ else:
56
+ commands.extend(
57
+ [
58
+ "catkin build # or: colcon build, depending on the ROS package type",
59
+ "source devel/setup.bash # or: source install/setup.bash",
60
+ ]
61
+ )
62
+
63
+ if "conda" in ecosystems:
64
+ env_name = "simfix-env"
65
+ if analysis.conda_environment is not None and analysis.conda_environment.name:
66
+ env_name = analysis.conda_environment.name
67
+
68
+ commands.extend(
69
+ [
70
+ "conda env create -f environment.yml",
71
+ f"conda activate {env_name}",
72
+ ]
73
+ )
74
+
75
+ if "python" in ecosystems:
76
+ commands.extend(
77
+ [
78
+ "python -m venv .venv",
79
+ "source .venv/bin/activate",
80
+ "python -m pip install --upgrade pip",
81
+ ]
82
+ )
83
+
84
+ if analysis.has_requirements_txt:
85
+ commands.append("python -m pip install -r requirements.txt")
86
+
87
+ if analysis.has_pyproject_toml or analysis.has_setup_py:
88
+ commands.extend(
89
+ [
90
+ "python -m pip install -e .",
91
+ "python -m pip install -e . --no-deps # use only if vendor/manual dependencies block normal install",
92
+ ]
93
+ )
94
+
95
+ if "cmake/c++" in ecosystems and "ros" not in ecosystems:
96
+ commands.extend(
97
+ [
98
+ "cmake -S . -B build",
99
+ "cmake --build build -j",
100
+ ]
101
+ )
102
+
103
+ if not commands:
104
+ commands.extend(
105
+ [
106
+ "# No common dependency file was detected.",
107
+ "# Read the README installation section manually.",
108
+ "# Check for install scripts such as install.sh or setup.sh.",
109
+ ]
110
+ )
111
+
112
+ return CommandPlan(
113
+ title="Suggested installation commands",
114
+ commands=_deduplicate_commands(commands),
115
+ )
116
+
117
+
118
+ def _deduplicate_commands(commands: list[str]) -> list[str]:
119
+ """Return commands without duplicates while preserving order."""
120
+ seen: set[str] = set()
121
+ unique_commands: list[str] = []
122
+
123
+ for command in commands:
124
+ if command in seen:
125
+ continue
126
+
127
+ seen.add(command)
128
+ unique_commands.append(command)
129
+
130
+ return unique_commands
@@ -0,0 +1,133 @@
1
+ """CUDA version detection helpers for SimFix recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CudaVersionInfo:
13
+ """Detected CUDA version information."""
14
+
15
+ repo_cuda_version: tuple[int, int] | None
16
+ system_cuda_version: tuple[int, int] | None
17
+ repo_cuda_source: str | None
18
+
19
+
20
+ def detect_cuda_version_info(repo_path: Path) -> CudaVersionInfo:
21
+ """Detect repository CUDA requirement and system CUDA capability."""
22
+ repo_cuda_version, repo_cuda_source = detect_repo_cuda_version(repo_path)
23
+ system_cuda_version = detect_system_cuda_version()
24
+
25
+ return CudaVersionInfo(
26
+ repo_cuda_version=repo_cuda_version,
27
+ system_cuda_version=system_cuda_version,
28
+ repo_cuda_source=repo_cuda_source,
29
+ )
30
+
31
+
32
+ def detect_repo_cuda_version(
33
+ repo_path: Path,
34
+ ) -> tuple[tuple[int, int] | None, str | None]:
35
+ """Detect CUDA version requested by repository files.
36
+
37
+ This is intentionally generic. It checks common simulator/deep-learning
38
+ signals such as Docker base images and CUDA-specific dependency names.
39
+ """
40
+ dockerfile = repo_path / "Dockerfile"
41
+ if dockerfile.exists():
42
+ version = _detect_cuda_version_from_text(dockerfile.read_text(encoding="utf-8"))
43
+ if version is not None:
44
+ return version, "Dockerfile"
45
+
46
+ dependency_files = [
47
+ repo_path / "requirements.txt",
48
+ repo_path / "pyproject.toml",
49
+ repo_path / "setup.py",
50
+ repo_path / "environment.yml",
51
+ repo_path / "environment.yaml",
52
+ ]
53
+
54
+ for dependency_file in dependency_files:
55
+ if dependency_file.exists():
56
+ version = _detect_cuda_version_from_text(
57
+ dependency_file.read_text(encoding="utf-8")
58
+ )
59
+ if version is not None:
60
+ return version, dependency_file.name
61
+
62
+ return None, None
63
+
64
+
65
+ def detect_system_cuda_version() -> tuple[int, int] | None:
66
+ """Detect CUDA version supported by the NVIDIA driver using nvidia-smi."""
67
+ try:
68
+ result = subprocess.run(
69
+ ["nvidia-smi"],
70
+ check=False,
71
+ capture_output=True,
72
+ text=True,
73
+ timeout=10,
74
+ )
75
+ except (OSError, subprocess.SubprocessError):
76
+ return None
77
+
78
+ return _parse_nvidia_smi_cuda_version(result.stdout + result.stderr)
79
+
80
+
81
+ def is_cuda_version_mismatch(
82
+ repo_cuda_version: tuple[int, int] | None,
83
+ system_cuda_version: tuple[int, int] | None,
84
+ ) -> bool:
85
+ """Return True if the repo appears to require newer CUDA than the system."""
86
+ if repo_cuda_version is None or system_cuda_version is None:
87
+ return False
88
+
89
+ return repo_cuda_version > system_cuda_version
90
+
91
+
92
+ def _detect_cuda_version_from_text(text: str) -> tuple[int, int] | None:
93
+ """Detect CUDA version from generic text."""
94
+ normalized = text.lower()
95
+
96
+ detected_versions: list[tuple[int, int]] = []
97
+
98
+ explicit_patterns = [
99
+ r"nvidia/cuda:(\d+)\.(\d+)",
100
+ r"cuda[-_]?(\d+)[\._-](\d+)",
101
+ ]
102
+
103
+ for pattern in explicit_patterns:
104
+ for match in re.finditer(pattern, normalized):
105
+ detected_versions.append((int(match.group(1)), int(match.group(2))))
106
+
107
+ for match in re.finditer(r"cuda[-_]?(\d+)x", normalized):
108
+ detected_versions.append((int(match.group(1)), 0))
109
+
110
+ for match in re.finditer(r"cu(\d{2,3})", normalized):
111
+ detected_versions.append(_parse_compact_cuda_version(match.group(1)))
112
+
113
+ if not detected_versions:
114
+ return None
115
+
116
+ return max(detected_versions)
117
+
118
+
119
+ def _parse_nvidia_smi_cuda_version(text: str) -> tuple[int, int] | None:
120
+ """Parse CUDA version from nvidia-smi output."""
121
+ match = re.search(r"cuda version:\s*(\d+)\.(\d+)", text.lower())
122
+ if match is None:
123
+ return None
124
+
125
+ return int(match.group(1)), int(match.group(2))
126
+
127
+
128
+ def _parse_compact_cuda_version(version: str) -> tuple[int, int]:
129
+ """Parse compact CUDA version strings such as cu118 or cu121."""
130
+ if len(version) == 2:
131
+ return int(version[0]), int(version[1])
132
+
133
+ return int(version[:-1]), int(version[-1])
@@ -0,0 +1,139 @@
1
+ """Generic dependency-file discovery for simulator repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ IGNORED_DIRECTORY_NAMES = {
10
+ ".git",
11
+ ".hg",
12
+ ".svn",
13
+ ".venv",
14
+ "venv",
15
+ "env",
16
+ "__pycache__",
17
+ ".mypy_cache",
18
+ ".ruff_cache",
19
+ ".pytest_cache",
20
+ "build",
21
+ "dist",
22
+ "install",
23
+ "log",
24
+ "node_modules",
25
+ }
26
+
27
+
28
+ DEPENDENCY_FILE_NAMES = {
29
+ "requirements.txt",
30
+ "pyproject.toml",
31
+ "environment.yml",
32
+ "environment.yaml",
33
+ "Dockerfile",
34
+ "package.xml",
35
+ "CMakeLists.txt",
36
+ "setup.py",
37
+ }
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class DiscoveredDependencyFiles:
42
+ """Dependency files discovered in a repository."""
43
+
44
+ requirements_txt: tuple[Path, ...]
45
+ pyproject_toml: tuple[Path, ...]
46
+ environment_files: tuple[Path, ...]
47
+ dockerfiles: tuple[Path, ...]
48
+ package_xml_files: tuple[Path, ...]
49
+ cmake_lists_files: tuple[Path, ...]
50
+ setup_py_files: tuple[Path, ...]
51
+
52
+ @property
53
+ def has_requirements_txt(self) -> bool:
54
+ return bool(self.requirements_txt)
55
+
56
+ @property
57
+ def has_pyproject_toml(self) -> bool:
58
+ return bool(self.pyproject_toml)
59
+
60
+ @property
61
+ def has_environment_file(self) -> bool:
62
+ return bool(self.environment_files)
63
+
64
+ @property
65
+ def has_dockerfile(self) -> bool:
66
+ return bool(self.dockerfiles)
67
+
68
+ @property
69
+ def has_ros_package(self) -> bool:
70
+ return bool(self.package_xml_files)
71
+
72
+ @property
73
+ def has_cmake_lists(self) -> bool:
74
+ return bool(self.cmake_lists_files)
75
+
76
+ @property
77
+ def has_setup_py(self) -> bool:
78
+ return bool(self.setup_py_files)
79
+
80
+
81
+ def discover_dependency_files(
82
+ repo_path: Path,
83
+ *,
84
+ max_depth: int = 4,
85
+ ) -> DiscoveredDependencyFiles:
86
+ """Discover dependency files in a repository up to a safe depth.
87
+
88
+ This is generic and does not assume a specific simulator layout.
89
+ It intentionally skips common build, cache, virtualenv, and VCS folders.
90
+ """
91
+ discovered_files = _walk_dependency_files(repo_path, max_depth=max_depth)
92
+
93
+ return DiscoveredDependencyFiles(
94
+ requirements_txt=_filter_by_name(discovered_files, "requirements.txt"),
95
+ pyproject_toml=_filter_by_name(discovered_files, "pyproject.toml"),
96
+ environment_files=tuple(
97
+ path
98
+ for path in discovered_files
99
+ if path.name in {"environment.yml", "environment.yaml"}
100
+ ),
101
+ dockerfiles=_filter_by_name(discovered_files, "Dockerfile"),
102
+ package_xml_files=_filter_by_name(discovered_files, "package.xml"),
103
+ cmake_lists_files=_filter_by_name(discovered_files, "CMakeLists.txt"),
104
+ setup_py_files=_filter_by_name(discovered_files, "setup.py"),
105
+ )
106
+
107
+
108
+ def _walk_dependency_files(repo_path: Path, *, max_depth: int) -> tuple[Path, ...]:
109
+ repo_path = repo_path.resolve()
110
+ found: list[Path] = []
111
+
112
+ for path in repo_path.rglob("*"):
113
+ if not path.is_file():
114
+ continue
115
+
116
+ relative_path = path.relative_to(repo_path)
117
+
118
+ if _should_skip_path(relative_path):
119
+ continue
120
+
121
+ if _depth(relative_path) > max_depth:
122
+ continue
123
+
124
+ if path.name in DEPENDENCY_FILE_NAMES:
125
+ found.append(path)
126
+
127
+ return tuple(sorted(found))
128
+
129
+
130
+ def _filter_by_name(paths: tuple[Path, ...], name: str) -> tuple[Path, ...]:
131
+ return tuple(path for path in paths if path.name == name)
132
+
133
+
134
+ def _should_skip_path(relative_path: Path) -> bool:
135
+ return any(part in IGNORED_DIRECTORY_NAMES for part in relative_path.parts)
136
+
137
+
138
+ def _depth(relative_path: Path) -> int:
139
+ return len(relative_path.parts) - 1