Skip to content

Commit baa1ee7

Browse files
davidkoppclaude
andcommitted
Implement multi-location detection for pip detector
Add conditional output structure enabling pip to detect packages from both virtual environment and system locations simultaneously. When packages exist in multiple locations, uses nested structure with location-specific metadata. Single location scenarios maintain existing output format for compatibility. Key changes: - Add _get_venv_dependencies() and _get_system_dependencies() methods - Implement conditional output: single location vs mixed scope structure - Update orchestrator to handle mixed scope results - Enhance test infrastructure to support both output formats generically - Add comprehensive ADR documenting architectural decision - Update documentation with examples and usage patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 656c828 commit baa1ee7

File tree

7 files changed

+371
-55
lines changed

7 files changed

+371
-55
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# ADR-0006: Multi-Location Detection for Package Managers
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
Package managers can have dependencies installed in multiple locations simultaneously. For example, pip can have packages installed in both a virtual environment and the system Python installation. The original detector architecture assumed each detector would return dependencies from a single location, missing packages in other locations.
10+
11+
**Problem**: The pip detector prioritized virtual environments over system installations, so when both existed, only virtual environment packages were detected. This prevented comprehensive dependency analysis in mixed environments.
12+
13+
**Alternatives Considered**:
14+
15+
1. **Split into separate detectors**: Create `pip-venv` and `pip-system` detectors
16+
- ❌ Breaks existing API expectations
17+
- ❌ Requires orchestrator changes
18+
- ❌ Confusing output with multiple pip entries
19+
20+
2. **Always merge dependencies**: Combine all packages into single result
21+
- ❌ Loses location information for each package
22+
- ❌ Cannot track location-specific hashes
23+
- ❌ Reduces traceability
24+
25+
3. **Universal nested structure**: Change all detectors to use locations format
26+
- ❌ Major breaking change
27+
- ❌ Unnecessary complexity for single-location detectors
28+
- ❌ Verbose output when most detectors have only one location
29+
- ❌ Inconsistent with the principle of simple structures for simple cases
30+
31+
4. **Conditional output structure**: Single location uses existing format, multiple locations use nested format
32+
- ✅ Preserves backward compatibility
33+
- ✅ Simple structure for simple cases
34+
- ✅ Maintains location-specific metadata
35+
- ✅ Clear semantic distinction
36+
- ✅ Efficient output format
37+
38+
## Decision
39+
40+
We will implement **conditional output structure** for multi-location detection:
41+
42+
### Single Location (Existing Format)
43+
44+
```json
45+
{
46+
"pip": {
47+
"scope": "project",
48+
"location": "/path/to/venv",
49+
"hash": "abc123...",
50+
"dependencies": {...}
51+
}
52+
}
53+
```
54+
55+
### Multiple Locations (New Format)
56+
57+
```json
58+
{
59+
"pip": {
60+
"scope": "mixed",
61+
"locations": {
62+
"/path/to/venv": {
63+
"scope": "project",
64+
"hash": "abc123...",
65+
"dependencies": {...}
66+
},
67+
"/usr/lib/python3/dist-packages": {
68+
"scope": "system",
69+
"hash": "def456...",
70+
"dependencies": {...}
71+
}
72+
}
73+
}
74+
}
75+
```
76+
77+
### Implementation Approach
78+
79+
1. **Detector Interface**: No changes to `PackageManagerDetector` interface
80+
2. **Conditional Logic**: Detectors return nested structure only when multiple locations exist
81+
3. **Orchestrator Support**: Enhanced to recognize `scope: "mixed"` pattern
82+
4. **Generalized Pattern**: Any detector can implement multi-location detection
83+
84+
## Consequences
85+
86+
### Positive
87+
88+
-**Backward Compatible**: Existing code continues to work unchanged
89+
-**Location Transparency**: Each package's source location is preserved
90+
-**Hash Preservation**: Location-specific hashes maintained
91+
-**Extensible**: Pattern available for future detectors (npm, maven, etc.)
92+
-**Self-Documenting**: Output structure indicates single vs. multi-location
93+
-**Efficient**: Simple structure for simple cases, complex structure only when needed
94+
95+
### Negative
96+
97+
-**Complexity**: Consumers must handle two different output formats
98+
-**Testing Overhead**: Additional test scenarios for mixed-scope handling
99+
100+
### Neutral
101+
102+
- **New Scope Value**: Introduces `"mixed"` as third scope option alongside `"system"` and `"project"`
103+
104+
## Implementation Notes
105+
106+
- **pip detector**: First implementation targeting venv + system detection
107+
- **Test Infrastructure**: Updated to handle both output formats generically
108+
- **Documentation**: Updated to explain conditional structure behavior
109+
- **Future Detectors**: npm could implement similar pattern for global vs. local node_modules
110+
111+
This approach solves the multi-location problem while minimizing breaking changes and maintaining clear traceability of package origins.

docs/technical/detectors/pip_detector.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,16 @@ For project-scoped dependencies, generates location-based hashes by scanning the
5252

5353
## Output Format
5454

55-
**Project Scope** (with virtual environment):
55+
**Single Location Detection:**
5656

57-
- Includes `scope: "project"`, installation location, and content hash
58-
- Contains only project-specific packages
57+
- **Project Scope** (venv only): `scope: "project"`, installation location, and content hash
58+
- **System Scope** (system only): `scope: "system"` with system-wide packages
5959

60-
**System Scope** (no virtual environment):
60+
**Multi-Location Detection:**
6161

62-
- Includes `scope: "system"`
63-
- Contains system-wide Python packages
62+
- **Mixed Scope** (venv + system): `scope: "mixed"` with nested `locations` structure
63+
- Each location preserves its own scope, dependencies, and location-specific hash
64+
- Enables tracking packages from multiple pip installations simultaneously
6465

6566
## Benefits
6667

docs/usage/output-format.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,46 @@ Project-specific package managers (pip, npm) output:
8585
}
8686
```
8787

88+
### Mixed-Scope Packages (Multi-Location Detection)
89+
90+
When a detector finds packages in multiple locations (currently only pip supports this):
91+
92+
```json
93+
{
94+
"pip": {
95+
"scope": "mixed",
96+
"locations": {
97+
"/root/venv/lib/python3.12/site-packages": {
98+
"scope": "project",
99+
"hash": "abc123...",
100+
"dependencies": {
101+
"venv-package": {
102+
"version": "1.0.0"
103+
}
104+
}
105+
},
106+
"/usr/local/lib/python3.12/dist-packages": {
107+
"scope": "system",
108+
"hash": "def456...",
109+
"dependencies": {
110+
"system-package": {
111+
"version": "2.0.0"
112+
}
113+
}
114+
}
115+
}
116+
}
117+
}
118+
```
119+
88120
## Field Definitions
89121

90122
### Common Fields
91123

92-
- **scope** - Either `"system"` or `"project"`
124+
- **scope** - Either `"system"`, `"project"`, or `"mixed"`
93125
- `system`: System-wide packages affecting the entire environment
94126
- `project`: Project-specific packages in a local scope
127+
- `mixed`: Packages from multiple locations (pip only)
95128

96129
- **dependencies** - Object containing all detected packages
97130
- Keys: Package names
@@ -246,7 +279,12 @@ system_packages = {k: v for k, v in deps.items()
246279
project_packages = {k: v for k, v in deps.items()
247280
if not k.startswith("_") and v.get("scope") == "project"}
248281

249-
# Count total packages
250-
total = sum(len(v.get("dependencies", {})) for v in deps.values()
251-
if not k.startswith("_"))
282+
# Handle mixed-scope packages (pip multi-location)
283+
def count_dependencies(result):
284+
if result.get("scope") == "mixed":
285+
return sum(len(loc.get("dependencies", {})) for loc in result.get("locations", {}).values())
286+
return len(result.get("dependencies", {}))
287+
288+
# Count total packages (including mixed-scope)
289+
total = sum(count_dependencies(v) for k, v in deps.items() if not k.startswith("_"))
252290
```

energy_dependency_inspector/core/orchestrator.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,21 @@ def resolve_dependencies(
9898
print(f"Found container info for {detector_name}")
9999
else:
100100
# Standard handling for other detectors
101-
if dependencies.get("dependencies") or self.debug:
101+
# Check if result has dependencies (single location) or locations (mixed scope structure)
102+
has_dependencies = dependencies.get("dependencies") or (
103+
dependencies.get("scope") == "mixed" and dependencies.get("locations")
104+
)
105+
if has_dependencies or self.debug:
102106
result[detector_name] = dependencies
103107

104108
if self.debug:
105-
dep_count = len(dependencies.get("dependencies", {}))
109+
if dependencies.get("scope") == "mixed":
110+
# Count dependencies across all locations for mixed scope
111+
dep_count = 0
112+
for location_data in dependencies.get("locations", {}).values():
113+
dep_count += len(location_data.get("dependencies", {}))
114+
else:
115+
dep_count = len(dependencies.get("dependencies", {}))
106116
print(f"Found {dep_count} dependencies for {detector_name}")
107117
else:
108118
if self.debug:

0 commit comments

Comments
 (0)