Skip to content

Commit 7ed88bc

Browse files
committed
Add new flag --select-detectors
1 parent 87e5adc commit 7ed88bc

File tree

8 files changed

+213
-9
lines changed

8 files changed

+213
-9
lines changed

TODOS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
- Rename CLI flag '--only-container-info' to '--only-source-info'
44
- Publish package to PyPI
55
- Add host_info_detector (similar to docker_info_detector)
6-
- Add CLI flag to select a subset of detectors (see how Syft does it: <https://github.com/anchore/syft/wiki/package-cataloger-selection>)
76
- Extend the set of supported package managers with the common ones for Go, PHP and Java
87
- Add Dockerfile
98
- Add support for Podman

dependency_resolver/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def parse_arguments() -> argparse.Namespace:
3737
%(prog)s --debug # Enable debug output
3838
%(prog)s --skip-system-scope # Skip system scope package managers
3939
%(prog)s --skip-hash-collection # Skip hash collection for improved performance
40+
%(prog)s --select-detectors "pip,dpkg" # Use only pip and dpkg detectors
4041
""",
4142
)
4243

@@ -85,6 +86,12 @@ def parse_arguments() -> argparse.Namespace:
8586
help="Skip hash collection for packages and project locations to improve performance",
8687
)
8788

89+
parser.add_argument(
90+
"--select-detectors",
91+
type=str,
92+
help="Comma-separated list of detectors to use (e.g., 'pip,dpkg'). Available: pip, npm, dpkg, apk, maven, docker-info",
93+
)
94+
8895
return parser.parse_args()
8996

9097

@@ -133,6 +140,7 @@ def main() -> None:
133140
skip_system_scope=args.skip_system_scope,
134141
venv_path=args.venv_path,
135142
skip_hash_collection=args.skip_hash_collection,
143+
selected_detectors=args.select_detectors,
136144
)
137145
dependencies = orchestrator.resolve_dependencies(executor, args.working_dir, args.only_container_info)
138146
formatter = OutputFormatter(debug=args.debug)

dependency_resolver/core/orchestrator.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ def __init__(
1717
skip_system_scope: bool = False,
1818
venv_path: str | None = None,
1919
skip_hash_collection: bool = False,
20+
selected_detectors: str | None = None,
2021
):
2122
self.debug = debug
2223
self.skip_system_scope = skip_system_scope
2324
self.skip_hash_collection = skip_hash_collection
2425

25-
# Create detector instances
26-
self.detectors: list[PackageManagerDetector] = [
26+
# Create all detector instances
27+
all_detectors: list[PackageManagerDetector] = [
2728
DockerInfoDetector(),
2829
DpkgDetector(),
2930
ApkDetector(),
@@ -32,6 +33,26 @@ def __init__(
3233
NpmDetector(debug=debug),
3334
]
3435

36+
# Filter detectors based on selection
37+
if selected_detectors:
38+
selected_names = [name.strip() for name in selected_detectors.split(",")]
39+
available_names = {detector.NAME for detector in all_detectors}
40+
41+
# Validate detector names
42+
invalid_names = [name for name in selected_names if name not in available_names]
43+
if invalid_names:
44+
raise ValueError(
45+
f"Invalid detector names: {', '.join(invalid_names)}. Available detectors: {', '.join(sorted(available_names))}"
46+
)
47+
48+
# Filter to only selected detectors
49+
self.detectors = [detector for detector in all_detectors if detector.NAME in selected_names]
50+
if self.debug:
51+
selected_detector_names = [detector.NAME for detector in self.detectors]
52+
print(f"Selected detectors: {', '.join(selected_detector_names)}")
53+
else:
54+
self.detectors = all_detectors
55+
3556
def resolve_dependencies(
3657
self, executor: EnvironmentExecutor, working_dir: Optional[str] = None, only_container_info: bool = False
3758
) -> dict[str, Any]:

docs/guides/troubleshooting.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,10 @@ npm --version
171171

172172
1. Use `--skip-system-scope` to skip system package managers
173173
2. Use `--skip-hash-collection` to skip hash generation for packages and locations
174-
3. Use `--only-container-info` for Docker metadata only
175-
4. Specify working directory to limit scope
176-
5. Check if system has many installed packages
174+
3. Use `--select-detectors` to analyze only specific package managers (e.g., `--select-detectors "pip,npm"`)
175+
4. Use `--only-container-info` for Docker metadata only
176+
5. Specify working directory to limit scope
177+
6. Check if system has many installed packages
177178

178179
## Getting Help
179180

docs/usage/cli-guide.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,35 @@ python3 -m dependency_resolver --skip-system-scope
4646

4747
# Skip hash collection for improved performance
4848
python3 -m dependency_resolver --skip-hash-collection
49+
50+
# Select specific detectors to run
51+
python3 -m dependency_resolver --select-detectors "pip,dpkg"
4952
```
5053

54+
## Detector Selection
55+
56+
Control which package managers are analyzed with the `--select-detectors` flag:
57+
58+
```bash
59+
# Use only pip and dpkg detectors
60+
python3 -m dependency_resolver --select-detectors "pip,dpkg"
61+
62+
# Use only npm detector for Node.js projects
63+
python3 -m dependency_resolver --select-detectors "npm"
64+
65+
# Use multiple detectors with debug output
66+
python3 -m dependency_resolver --select-detectors "pip,npm,maven" --debug
67+
```
68+
69+
**Available detectors:**
70+
71+
- `pip` - Python packages (pip/PyPI)
72+
- `npm` - Node.js packages (npm/yarn)
73+
- `dpkg` - Debian/Ubuntu system packages
74+
- `apk` - Alpine Linux system packages
75+
- `maven` - Java Maven dependencies
76+
- `docker-info` - Docker container metadata
77+
5178
## Common Usage Patterns
5279

5380
### Analyzing a Specific Project
@@ -60,6 +87,9 @@ python3 -m dependency_resolver --working-dir /path/to/python/project
6087

6188
# Analyze Node.js project
6289
python3 -m dependency_resolver --working-dir /path/to/nodejs/project
90+
91+
# Analyze only Python dependencies in a project
92+
python3 -m dependency_resolver --working-dir /path/to/python/project --select-detectors "pip"
6393
```
6494

6595
### Comprehensive Docker Analysis

docs/usage/programmatic-api.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ from dependency_resolver import Orchestrator, HostExecutor, DockerExecutor, Outp
6868

6969
# Host system analysis with custom settings
7070
executor = HostExecutor()
71-
orchestrator = Orchestrator(debug=True, skip_system_scope=True, skip_hash_collection=True)
71+
orchestrator = Orchestrator(
72+
debug=True,
73+
skip_system_scope=True,
74+
skip_hash_collection=True,
75+
selected_detectors="pip,npm" # Only analyze Python and Node.js dependencies
76+
)
7277
dependencies = orchestrator.resolve_dependencies(executor)
7378

7479
# Format output
@@ -84,7 +89,12 @@ from dependency_resolver import Orchestrator, DockerExecutor, OutputFormatter
8489
# Docker container analysis
8590
container_id = "nginx"
8691
executor = DockerExecutor(container_id)
87-
orchestrator = Orchestrator(debug=False, skip_system_scope=False, skip_hash_collection=False)
92+
orchestrator = Orchestrator(
93+
debug=False,
94+
skip_system_scope=False,
95+
skip_hash_collection=False,
96+
selected_detectors="dpkg,docker-info" # Analyze system packages and container info only
97+
)
8898

8999
dependencies = orchestrator.resolve_dependencies(executor, working_dir="/app")
90100

@@ -115,10 +125,30 @@ orchestrator = Orchestrator(
115125
debug=True, # Enable debug output
116126
skip_system_scope=False, # Skip system-wide package managers
117127
venv_path="/path/to/venv", # Specify Python virtual environment path
118-
skip_hash_collection=False # Skip hash collection for improved performance
128+
skip_hash_collection=False, # Skip hash collection for improved performance
129+
selected_detectors="pip,npm" # Use only specific detectors
119130
)
120131
```
121132

133+
### Detector Selection
134+
135+
Control which package managers are analyzed by specifying the `selected_detectors` parameter:
136+
137+
```python
138+
from dependency_resolver import Orchestrator, HostExecutor
139+
140+
# Use only Python pip detector
141+
orchestrator = Orchestrator(selected_detectors="pip")
142+
143+
# Use multiple specific detectors
144+
orchestrator = Orchestrator(selected_detectors="pip,npm,maven")
145+
146+
# Use all detectors (default behavior)
147+
orchestrator = Orchestrator(selected_detectors=None)
148+
```
149+
150+
**Available detectors:** `pip`, `npm`, `dpkg`, `apk`, `maven`, `docker-info`
151+
122152
### Executor Options
123153

124154
```python

tests/core/test_orchestrator.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import pytest
2+
from pytest import CaptureFixture
3+
from dependency_resolver.core.orchestrator import Orchestrator
4+
5+
6+
class TestOrchestrator:
7+
"""Test cases for the Orchestrator class."""
8+
9+
def test_orchestrator_default_detectors(self) -> None:
10+
"""Test that orchestrator initializes with all detectors by default."""
11+
orchestrator = Orchestrator()
12+
13+
# Should have all 6 detectors
14+
assert len(orchestrator.detectors) == 6
15+
16+
detector_names = {detector.NAME for detector in orchestrator.detectors}
17+
expected_names = {"docker-info", "dpkg", "apk", "maven", "pip", "npm"}
18+
assert detector_names == expected_names
19+
20+
def test_orchestrator_select_valid_detectors(self) -> None:
21+
"""Test selecting valid detectors."""
22+
orchestrator = Orchestrator(selected_detectors="pip,dpkg")
23+
24+
# Should have only 2 detectors
25+
assert len(orchestrator.detectors) == 2
26+
27+
detector_names = {detector.NAME for detector in orchestrator.detectors}
28+
expected_names = {"pip", "dpkg"}
29+
assert detector_names == expected_names
30+
31+
def test_orchestrator_select_single_detector(self) -> None:
32+
"""Test selecting a single detector."""
33+
orchestrator = Orchestrator(selected_detectors="npm")
34+
35+
# Should have only 1 detector
36+
assert len(orchestrator.detectors) == 1
37+
assert orchestrator.detectors[0].NAME == "npm"
38+
39+
def test_orchestrator_select_all_detectors(self) -> None:
40+
"""Test selecting all detectors explicitly."""
41+
orchestrator = Orchestrator(selected_detectors="pip,npm,dpkg,apk,maven,docker-info")
42+
43+
# Should have all 6 detectors
44+
assert len(orchestrator.detectors) == 6
45+
46+
detector_names = {detector.NAME for detector in orchestrator.detectors}
47+
expected_names = {"docker-info", "dpkg", "apk", "maven", "pip", "npm"}
48+
assert detector_names == expected_names
49+
50+
def test_orchestrator_invalid_detector_name(self) -> None:
51+
"""Test that invalid detector names raise ValueError."""
52+
with pytest.raises(ValueError) as exc_info:
53+
Orchestrator(selected_detectors="pip,invalid-detector")
54+
55+
error_message = str(exc_info.value)
56+
assert "Invalid detector names: invalid-detector" in error_message
57+
assert "Available detectors:" in error_message
58+
59+
def test_orchestrator_multiple_invalid_detector_names(self) -> None:
60+
"""Test that multiple invalid detector names are reported."""
61+
with pytest.raises(ValueError) as exc_info:
62+
Orchestrator(selected_detectors="pip,invalid1,invalid2")
63+
64+
error_message = str(exc_info.value)
65+
assert "Invalid detector names: invalid1, invalid2" in error_message
66+
67+
def test_orchestrator_whitespace_handling(self) -> None:
68+
"""Test that whitespace in detector names is handled correctly."""
69+
orchestrator = Orchestrator(selected_detectors=" pip , dpkg ")
70+
71+
# Should have 2 detectors despite whitespace
72+
assert len(orchestrator.detectors) == 2
73+
74+
detector_names = {detector.NAME for detector in orchestrator.detectors}
75+
expected_names = {"pip", "dpkg"}
76+
assert detector_names == expected_names
77+
78+
def test_orchestrator_empty_selection(self) -> None:
79+
"""Test that empty string selection uses all detectors."""
80+
orchestrator = Orchestrator(selected_detectors="")
81+
82+
# Should have all 6 detectors (empty string is falsy)
83+
assert len(orchestrator.detectors) == 6
84+
85+
def test_orchestrator_none_selection(self) -> None:
86+
"""Test that None selection uses all detectors."""
87+
orchestrator = Orchestrator(selected_detectors=None)
88+
89+
# Should have all 6 detectors
90+
assert len(orchestrator.detectors) == 6
91+
92+
def test_orchestrator_debug_output_for_selected_detectors(self, capsys: CaptureFixture[str]) -> None:
93+
"""Test that debug output shows selected detectors."""
94+
Orchestrator(debug=True, selected_detectors="pip,npm")
95+
96+
captured = capsys.readouterr()
97+
assert "Selected detectors: pip, npm" in captured.out

tests/integration/test_cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,21 @@ def test_create_docker_executor_without_identifier_fails(self) -> None:
3838
"""Test docker executor creation fails without identifier."""
3939
with pytest.raises(SystemExit):
4040
create_executor("docker", None)
41+
42+
def test_select_detectors_argument(self) -> None:
43+
"""Test --select-detectors argument parsing."""
44+
with patch.object(sys, "argv", ["dependency_resolver", "--select-detectors", "pip,dpkg"]):
45+
args = parse_arguments()
46+
assert args.select_detectors == "pip,dpkg"
47+
48+
def test_select_detectors_single(self) -> None:
49+
"""Test --select-detectors with single detector."""
50+
with patch.object(sys, "argv", ["dependency_resolver", "--select-detectors", "npm"]):
51+
args = parse_arguments()
52+
assert args.select_detectors == "npm"
53+
54+
def test_select_detectors_default_none(self) -> None:
55+
"""Test --select-detectors defaults to None."""
56+
with patch.object(sys, "argv", ["dependency_resolver"]):
57+
args = parse_arguments()
58+
assert args.select_detectors is None

0 commit comments

Comments
 (0)