Skip to content

Commit 360568e

Browse files
committed
Merge branch 'main' into performance-optimization
2 parents 4d92f47 + 820fa4f commit 360568e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+930
-2290
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pip install -r requirements.txt
1515

1616
# Run specific detectors for testing
1717
python3 -m dependency_resolver docker <container_name>
18-
python3 -m dependency_resolver host --skip-system-scope --skip-hash-generation
18+
python3 -m dependency_resolver host --skip-os-packages --skip-hash-generation
1919

2020
# Execute linting and formatting
2121
pre-commit run --files $(git diff --name-only --diff-filter=ACMR HEAD)

SPECIFICATION.md

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,35 +60,23 @@ For complete architecture documentation, see [docs/technical/architecture/overvi
6060

6161
### JSON Output Schema
6262

63-
The tool outputs structured JSON with packages aggregated by scope (project/system):
63+
The tool outputs structured JSON with detected package managers and their dependencies:
6464

6565
```json
6666
{
67-
"source": { "type": "container", "name": "nginx-container", "image": "nginx:latest", "hash": "sha256:..." },
68-
"project": {
69-
"packages": [
70-
{ "name": "numpy", "version": "1.3.3", "type": "pip" }
71-
],
72-
"package-management": {
73-
"pip": { "location": "/app/venv/lib/python3.12/site-packages", "hash": "sha256:..." }
74-
}
75-
},
76-
"system": {
77-
"packages": [
78-
{ "name": "curl", "version": "7.81.0-1ubuntu1.18 amd64", "type": "dpkg", "hash": "abc123..." }
79-
]
80-
}
67+
"source": { "name": "nginx-container", "image": "nginx:latest", "hash": "sha256:..." },
68+
"dpkg": { "scope": "system", "dependencies": { "curl": { "version": "7.81.0-1ubuntu1.18 amd64" } } },
69+
"pip": { "scope": "project", "location": "/app/venv/lib/python3.12/site-packages", "dependencies": { "numpy": { "version": "1.3.3" } } }
8170
}
8271
```
8372

8473
For complete JSON schema documentation, field definitions, and examples, see [docs/usage/output-format.md](docs/usage/output-format.md).
8574

8675
### Key Schema Concepts
8776

88-
- **Project Packages**: All project-scoped packages aggregated in `project.packages[]` array
89-
- **System Packages**: All system-scoped packages aggregated in `system.packages[]` array
90-
- **Package Type**: Each package includes `type` field (`pip`, `npm`, `dpkg`, `apk`, `maven`)
91-
- **Metadata**: Package manager metadata stored in `project.{manager}` sections (location, hash)
77+
- **Scope**: `"system"` (system-wide packages) or `"project"` (project-specific packages)
78+
- **Location**: Path to project-local installations (project scope only)
79+
- **Hash**: Dependency integrity verification when available
9280
- **Source Info**: Environment metadata included as `source` when applicable (includes `type` field for container/host)
9381

9482
## Implementation Constraints

dependency_resolver/__init__.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def resolve_host_dependencies(
1212
working_dir: Optional[str] = None,
1313
debug: bool = False,
14-
skip_system_scope: bool = False,
14+
skip_os_packages: bool = False,
1515
venv_path: Optional[str] = None,
1616
pretty_print: bool = False,
1717
) -> str:
@@ -21,15 +21,15 @@ def resolve_host_dependencies(
2121
Args:
2222
working_dir: Working directory to analyze (defaults to current directory)
2323
debug: Enable debug output
24-
skip_system_scope: Skip system-scope package managers
24+
skip_os_packages: Skip OS package managers (dpkg, apk)
2525
venv_path: Explicit virtual environment path for pip detector
2626
pretty_print: Format JSON output with indentation
2727
2828
Returns:
2929
JSON string containing all discovered dependencies
3030
"""
3131
executor = HostExecutor(debug=debug)
32-
orchestrator = Orchestrator(debug=debug, skip_system_scope=skip_system_scope, venv_path=venv_path)
32+
orchestrator = Orchestrator(debug=debug, skip_os_packages=skip_os_packages, venv_path=venv_path)
3333
dependencies = orchestrator.resolve_dependencies(executor, working_dir)
3434
formatter = OutputFormatter(debug=debug)
3535
return formatter.format_json(dependencies, pretty_print=pretty_print)
@@ -39,7 +39,7 @@ def resolve_docker_dependencies(
3939
container_identifier: str,
4040
working_dir: Optional[str] = None,
4141
debug: bool = False,
42-
skip_system_scope: bool = False,
42+
skip_os_packages: bool = False,
4343
venv_path: Optional[str] = None,
4444
only_container_info: bool = False,
4545
pretty_print: bool = False,
@@ -51,7 +51,7 @@ def resolve_docker_dependencies(
5151
container_identifier: Container ID or name
5252
working_dir: Working directory to analyze within the container
5353
debug: Enable debug output
54-
skip_system_scope: Skip system-scope package managers
54+
skip_os_packages: Skip OS package managers (dpkg, apk)
5555
venv_path: Explicit virtual environment path for pip detector
5656
only_container_info: Only analyze container metadata (skip dependency detection)
5757
pretty_print: Format JSON output with indentation
@@ -60,7 +60,7 @@ def resolve_docker_dependencies(
6060
JSON string containing all discovered dependencies
6161
"""
6262
executor = DockerExecutor(container_identifier, debug=debug)
63-
orchestrator = Orchestrator(debug=debug, skip_system_scope=skip_system_scope, venv_path=venv_path)
63+
orchestrator = Orchestrator(debug=debug, skip_os_packages=skip_os_packages, venv_path=venv_path)
6464
dependencies = orchestrator.resolve_dependencies(executor, working_dir, only_container_info)
6565
formatter = OutputFormatter(debug=debug)
6666
return formatter.format_json(dependencies, pretty_print=pretty_print)
@@ -70,7 +70,7 @@ def resolve_docker_dependencies_as_dict(
7070
container_identifier: str,
7171
working_dir: Optional[str] = None,
7272
debug: bool = False,
73-
skip_system_scope: bool = False,
73+
skip_os_packages: bool = False,
7474
venv_path: Optional[str] = None,
7575
only_container_info: bool = False,
7676
) -> dict[str, Any]:
@@ -84,7 +84,7 @@ def resolve_docker_dependencies_as_dict(
8484
container_identifier: Container ID or name
8585
working_dir: Working directory to analyze within the container
8686
debug: Enable debug output
87-
skip_system_scope: Skip system-scope package managers
87+
skip_os_packages: Skip OS package managers (dpkg, apk)
8888
venv_path: Explicit virtual environment path for pip detector
8989
only_container_info: Only analyze container metadata (skip dependency detection)
9090
@@ -99,7 +99,7 @@ def resolve_docker_dependencies_as_dict(
9999
raise ValueError("Container identifier is required")
100100

101101
executor = DockerExecutor(container_identifier, debug=debug)
102-
orchestrator = Orchestrator(debug=debug, skip_system_scope=skip_system_scope, venv_path=venv_path)
102+
orchestrator = Orchestrator(debug=debug, skip_os_packages=skip_os_packages, venv_path=venv_path)
103103
return orchestrator.resolve_dependencies(executor, working_dir, only_container_info)
104104

105105

@@ -108,7 +108,7 @@ def resolve_dependencies_as_dict(
108108
environment_identifier: Optional[str] = None,
109109
working_dir: Optional[str] = None,
110110
debug: bool = False,
111-
skip_system_scope: bool = False,
111+
skip_os_packages: bool = False,
112112
venv_path: Optional[str] = None,
113113
only_container_info: bool = False,
114114
) -> dict[str, Any]:
@@ -120,7 +120,7 @@ def resolve_dependencies_as_dict(
120120
environment_identifier: Environment identifier (required for docker)
121121
working_dir: Working directory to analyze
122122
debug: Enable debug output
123-
skip_system_scope: Skip system-scope package managers
123+
skip_os_packages: Skip OS package managers (dpkg, apk)
124124
venv_path: Explicit virtual environment path for pip detector
125125
only_container_info: Only analyze container metadata (for docker environments)
126126
@@ -137,7 +137,7 @@ def resolve_dependencies_as_dict(
137137
else:
138138
raise ValueError(f"Unsupported environment type: {environment_type}")
139139

140-
orchestrator = Orchestrator(debug=debug, skip_system_scope=skip_system_scope, venv_path=venv_path)
140+
orchestrator = Orchestrator(debug=debug, skip_os_packages=skip_os_packages, venv_path=venv_path)
141141
return orchestrator.resolve_dependencies(executor, working_dir, only_container_info)
142142

143143

dependency_resolver/__main__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def parse_arguments() -> argparse.Namespace:
3535
%(prog)s --working-dir /tmp/repo # Set working directory on target environment
3636
%(prog)s --venv-path ~/.virtualenvs/myproject # Use specific virtual environment for pip
3737
%(prog)s --debug # Enable debug output
38-
%(prog)s --skip-system-scope # Skip system scope package managers
38+
%(prog)s --skip-os-packages # Skip OS package managers (dpkg, apk)
3939
%(prog)s --skip-hash-collection # Skip hash collection for improved performance
4040
%(prog)s --select-detectors "pip,dpkg" # Use only pip and dpkg detectors
4141
""",
@@ -63,9 +63,9 @@ def parse_arguments() -> argparse.Namespace:
6363
parser.add_argument("--debug", action="store_true", help="Print debug statements")
6464

6565
parser.add_argument(
66-
"--skip-system-scope",
66+
"--skip-os-packages",
6767
action="store_true",
68-
help="Skip system scope package managers (system packages or globally installed Python packages)",
68+
help="Skip OS package managers (dpkg, apk) - language package managers like pip/npm will still run",
6969
)
7070

7171
parser.add_argument(
@@ -137,7 +137,7 @@ def main() -> None:
137137
executor = create_executor(args.environment_type, args.environment_identifier, debug=args.debug)
138138
orchestrator = Orchestrator(
139139
debug=args.debug,
140-
skip_system_scope=args.skip_system_scope,
140+
skip_os_packages=args.skip_os_packages,
141141
venv_path=args.venv_path,
142142
skip_hash_collection=args.skip_hash_collection,
143143
selected_detectors=args.select_detectors,

dependency_resolver/core/interfaces.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def is_usable(self, executor: EnvironmentExecutor, working_dir: Optional[str] =
3333
@abstractmethod
3434
def get_dependencies(
3535
self, executor: EnvironmentExecutor, working_dir: Optional[str] = None, skip_hash_collection: bool = False
36-
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
36+
) -> dict[str, Any]:
3737
"""Extract dependencies with versions and hashes.
3838
3939
Args:
@@ -49,10 +49,10 @@ def get_dependencies(
4949
raise NotImplementedError
5050

5151
@abstractmethod
52-
def has_system_scope(self, executor: EnvironmentExecutor, working_dir: Optional[str] = None) -> bool:
53-
"""Check if this detector operates at system scope for the given environment.
52+
def is_os_package_manager(self) -> bool:
53+
"""Check if this detector manages OS-level packages (like dpkg, apk).
5454
55-
Returns True if the detector would return scope: "system" in get_dependencies().
56-
This allows efficient scope checking without the overhead of dependency extraction.
55+
Returns True for OS package managers that install system packages.
56+
Returns False for language package managers (pip, npm, maven) and other detectors.
5757
"""
5858
raise NotImplementedError

dependency_resolver/core/orchestrator.py

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ class Orchestrator:
1414
def __init__(
1515
self,
1616
debug: bool = False,
17-
skip_system_scope: bool = False,
17+
skip_os_packages: bool = False,
1818
venv_path: str | None = None,
1919
skip_hash_collection: bool = False,
2020
selected_detectors: str | None = None,
2121
):
2222
self.debug = debug
23-
self.skip_system_scope = skip_system_scope
23+
self.skip_os_packages = skip_os_packages
2424
self.skip_hash_collection = skip_hash_collection
2525

2626
# Create all detector instances
@@ -62,10 +62,6 @@ def resolve_dependencies(
6262
raise ValueError(f"Working directory does not exist: {working_dir}")
6363

6464
result: dict[str, Any] = {}
65-
project_packages: list[dict[str, Any]] = []
66-
project_metadata: dict[str, dict[str, Any]] = {}
67-
system_packages: list[dict[str, Any]] = []
68-
system_metadata: dict[str, dict[str, Any]] = {}
6965

7066
if only_container_info:
7167
# Only run docker-info detector when only container info is requested
@@ -81,45 +77,33 @@ def resolve_dependencies(
8177

8278
try:
8379
if detector.is_usable(executor, working_dir):
84-
# Check if detector has system scope and skip if requested
85-
if self.skip_system_scope and detector.has_system_scope(executor, working_dir):
80+
# Check if detector is OS package manager and skip if requested
81+
if self.skip_os_packages and detector.is_os_package_manager():
8682
if self.debug:
87-
print(f"Skipping {detector_name} (system scope, --skip-system-scope enabled)")
83+
print(f"Skipping {detector_name} (OS package manager, --skip-os-packages enabled)")
8884
continue
8985

9086
if self.debug:
9187
print(f"{detector_name} is usable, extracting dependencies...")
9288

93-
# Special handling for docker-info detector
89+
dependencies = detector.get_dependencies(
90+
executor, working_dir, skip_hash_collection=self.skip_hash_collection
91+
)
92+
93+
# Special handling for docker-info detector (simplified format)
9494
if detector_name == "docker-info":
95-
packages, metadata = detector.get_dependencies(
96-
executor, working_dir, skip_hash_collection=self.skip_hash_collection
97-
)
98-
if metadata: # Only include if we got container info
99-
metadata["type"] = "container"
100-
result["source"] = metadata
95+
result["source"] = dependencies
96+
result["source"]["type"] = "container"
10197
if self.debug:
10298
print(f"Found container info for {detector_name}")
10399
else:
104-
# Standard handling for package detectors
105-
packages, metadata = detector.get_dependencies(
106-
executor, working_dir, skip_hash_collection=self.skip_hash_collection
107-
)
108-
109-
if detector.has_system_scope(executor, working_dir):
110-
# System scope packages
111-
system_packages.extend(packages)
112-
if metadata:
113-
system_metadata[detector_name] = metadata
114-
if self.debug:
115-
print(f"Found {len(packages)} system packages for {detector_name}")
116-
else:
117-
# Project scope packages
118-
project_packages.extend(packages)
119-
if metadata:
120-
project_metadata[detector_name] = metadata
121-
if self.debug:
122-
print(f"Found {len(packages)} project packages for {detector_name}")
100+
# Standard handling for other detectors
101+
if dependencies.get("dependencies") or self.debug:
102+
result[detector_name] = dependencies
103+
104+
if self.debug:
105+
dep_count = len(dependencies.get("dependencies", {}))
106+
print(f"Found {dep_count} dependencies for {detector_name}")
123107
else:
124108
if self.debug:
125109
print(f"{detector_name} is not available")
@@ -129,13 +113,4 @@ def resolve_dependencies(
129113
print(f"Error checking {detector_name}: {str(e)}")
130114
continue
131115

132-
# Build final result structure matching proposal
133-
if project_packages or project_metadata:
134-
project_section: dict[str, Any] = {"packages": project_packages, "package-management": project_metadata}
135-
result["project"] = project_section
136-
137-
if system_packages or system_metadata:
138-
system_section: dict[str, Any] = {"packages": system_packages, "package-management": system_metadata}
139-
result["system"] = system_section
140-
141116
return result

dependency_resolver/core/output_formatter.py

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,30 @@ def format_json(self, dependencies: dict[str, Any], pretty_print: bool = True) -
2222
else:
2323
return json.dumps(dependencies)
2424

25-
def create_excerpt(self, dependencies: dict[str, Any], max_deps_per_section: int = 3) -> dict[str, Any]:
25+
def create_excerpt(self, dependencies: dict[str, Any], max_deps_per_manager: int = 3) -> dict[str, Any]:
2626
"""Create an excerpt of dependencies for debug mode."""
2727
excerpt: dict[str, Any] = {}
2828

29-
for section_name, section_data in dependencies.items():
30-
if section_name.startswith("_"):
31-
# Copy container info and other metadata as-is
32-
excerpt[section_name] = section_data
33-
elif section_name in ("project", "system"):
34-
# Handle new unified structure with packages array
35-
excerpt[section_name] = {}
36-
37-
for key, value in section_data.items():
38-
if key != "packages":
39-
# Copy metadata (package-management and other metadata)
40-
excerpt[section_name][key] = value
41-
42-
if "packages" in section_data:
43-
packages = section_data["packages"]
44-
total_packages = len(packages)
45-
46-
if total_packages <= max_deps_per_section:
47-
excerpt[section_name]["packages"] = packages
48-
else:
49-
limited_packages = packages[:max_deps_per_section]
50-
excerpt[section_name]["packages"] = limited_packages
51-
excerpt[section_name]["_excerpt_info"] = {
52-
"total_packages": total_packages,
53-
"shown": max_deps_per_section,
54-
"note": f"Showing {max_deps_per_section} of {total_packages} packages (debug mode excerpt)",
55-
}
56-
else:
57-
# Fallback for any legacy structure
58-
excerpt[section_name] = section_data
29+
for manager_name, manager_data in dependencies.items():
30+
excerpt[manager_name] = {}
31+
32+
for key, value in manager_data.items():
33+
if key != "dependencies":
34+
excerpt[manager_name][key] = value
35+
36+
if "dependencies" in manager_data:
37+
deps = manager_data["dependencies"]
38+
total_deps = len(deps)
39+
40+
if total_deps <= max_deps_per_manager:
41+
excerpt[manager_name]["dependencies"] = deps
42+
else:
43+
limited_deps = dict(list(deps.items())[:max_deps_per_manager])
44+
excerpt[manager_name]["dependencies"] = limited_deps
45+
excerpt[manager_name]["_excerpt_info"] = {
46+
"total_dependencies": total_deps,
47+
"shown": max_deps_per_manager,
48+
"note": f"Showing {max_deps_per_manager} of {total_deps} dependencies (debug mode excerpt)",
49+
}
5950

6051
return excerpt

0 commit comments

Comments
 (0)