Repository navigation
feat/0.10.0: NpmMetadataAdapter + BrewMetadataAdapter #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0ce2fc2
5d84c42
6d40387
daebbcc
e9fa7a3
15cb824
fe9ef16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,8 @@ def get_adapter(manager: str): | |
| "apt": APTMetadataAdapter, | ||
| "pip": PipMetadataAdapter, | ||
| "pacman": PacmanMetadataAdapter, | ||
| "npm": NpmMetadataAdapter, | ||
| "brew": BrewMetadataAdapter, | ||
| } | ||
| cls = adapters.get(manager.lower()) | ||
| if cls is None: | ||
|
|
@@ -341,3 +343,166 @@ def _parse_pacman_deps(self, raw: str) -> list[str]: | |
| deps.append(dep) | ||
|
|
||
| return deps | ||
|
|
||
|
|
||
| class NpmMetadataAdapter: | ||
| """Парсер метаданных npm (package-lock.json v2/v3).""" | ||
|
|
||
| def fetch(self, package_name: str) -> PackageMetadata | None: | ||
| """Получить метаданные через npm view.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["npm", "view", package_name, "json"], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
npm view express json
npm view express --json | python -m json.tool >/dev/nullRepository: dominicusin/package-maximizer Length of output: 791 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- adapter definitions and fetch path ---'
sed -n '1,220p' package_maximizer/adapters/__init__.py
sed -n '320,390p' package_maximizer/adapters/__init__.py
printf '%s\n' '--- affected test ---'
sed -n '70,125p' tests/test_adapters_extra.py
printf '%s\n' '--- changelog entry ---'
sed -n '1,30p' CHANGELOG.md
printf '%s\n' '--- relevant command and result handling references ---'
rg -n -C 3 'subprocess\.run|fetch\(|npm|returncode|stdout|json\.loads' package_maximizer testsRepository: dominicusin/package-maximizer Length of output: 50385 Use npm’s
Change the command, test expectation, and changelog entry to use 🧰 Tools🪛 ast-grep (0.45.2)[error] 353-358: Command coming from incoming request (subprocess-from-request) 🪛 Ruff (0.16.3)[error] 355-355: Starting a process with a partial executable path (S607) 📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| capture_output=True, | ||
| text=True, | ||
| timeout=10, | ||
| ) | ||
| if result.returncode == 0: | ||
| return self.parse(result.stdout) | ||
| except (subprocess.TimeoutExpired, FileNotFoundError): | ||
| pass | ||
| return None | ||
|
|
||
| def parse(self, raw: str) -> PackageMetadata | None: | ||
| """Разобрать вывод npm view или package-lock.json.""" | ||
| if not raw.strip(): | ||
| return None | ||
|
|
||
| import json | ||
|
|
||
| try: | ||
| data = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| return None | ||
|
|
||
| # Handle both single package and array responses | ||
| if isinstance(data, list): | ||
| if len(data) == 0: | ||
| return None | ||
| data = data[-1] # Latest version | ||
|
|
||
| if not isinstance(data, dict): | ||
| return None | ||
|
|
||
| metadata = PackageMetadata( | ||
| name=data.get("name", ""), | ||
| version=data.get("version", ""), | ||
| description=data.get("description", ""), | ||
| homepage=data.get("homepage", ""), | ||
| ) | ||
|
|
||
| # Dependencies | ||
| deps = data.get("dependencies", {}) | ||
| if isinstance(deps, dict): | ||
| metadata.depends.extend(deps.keys()) | ||
|
|
||
| # Conflicts (npm uses "conflicts" rarely, but check) | ||
| conflicts = data.get("conflicts", {}) | ||
| if isinstance(conflicts, dict): | ||
| metadata.conflicts.extend(conflicts.keys()) | ||
|
|
||
| return metadata if metadata.name else None | ||
|
|
||
| def parse_lockfile(self, raw: str) -> list[PackageMetadata]: | ||
| """Разобрать package-lock.json v2/v3.""" | ||
| if not raw.strip(): | ||
| return [] | ||
|
|
||
| import json | ||
|
|
||
| try: | ||
| data = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| return [] | ||
|
|
||
| packages = [] | ||
| packages_data = data.get("packages", {}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Validate the decoded lockfile shape before reading it. A valid JSON value such as 🤖 Prompt for AI Agents |
||
|
|
||
| for path, info in packages_data.items(): | ||
| if not isinstance(info, dict): | ||
| continue | ||
| # Skip the root entry | ||
| if path == "": | ||
| continue | ||
|
|
||
| name = info.get("name", "") | ||
| 8BD0 if not name: | ||
| # Extract from path (node_modules/@scope/pkg or node_modules/pkg) | ||
| parts = path.replace("node_modules/", "").split("/") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
file=$(git ls-files | awk '$0=="package_maximizer/adapters/__init__.py"{print; exit}')
[ -n "$file" ]
printf '%s\n' "$file"
sed -n '380,455p' "$file"
printf '\n-- related symbols and tests --\n'
rg -n -C 3 'class (NpmMetadataAdapter|BrewMetadataAdapter)|def (fetch|parse_lockfile)|node_modules/|NpmMetadataAdapter|BrewMetadataAdapter' . --glob '!*.lock' --glob '!node_modules/**'Repository: dominicusin/package-maximizer Length of output: 17147 Extract the package name from the final When an npm lockfile entry omits 🤖 Prompt for AI Agents |
||
| if len(parts) > 1 and parts[0].startswith("@"): | ||
| name = "/".join(parts[:2]) | ||
| else: | ||
| name = parts[0] | ||
|
|
||
| metadata = PackageMetadata( | ||
| name=name, | ||
| version=info.get("version", ""), | ||
| ) | ||
|
|
||
| deps = info.get("dependencies", {}) | ||
| if isinstance(deps, dict): | ||
| metadata.depends.extend(deps.keys()) | ||
|
|
||
| packages.append(metadata) | ||
|
|
||
| return packages | ||
|
|
||
|
|
||
| class BrewMetadataAdapter: | ||
| """Парсер метаданных brew (brew info --json=v2).""" | ||
|
|
||
| def fetch(self, package_name: str) -> PackageMetadata | None: | ||
| """Получить метаданные через brew info --json=v2.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["brew", "info", "--json=v2", package_name], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=10, | ||
| ) | ||
| if result.returncode == 0: | ||
| import json | ||
|
|
||
| data = json.loads(result.stdout) | ||
| if data and isinstance(data, list) and len(data) > 0: | ||
| return self._parse_brew_json(data[0]) | ||
| except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError): | ||
| pass | ||
| return None | ||
|
|
||
| def _parse_brew_json(self, data: dict) -> PackageMetadata: | ||
| """Разобрать JSON от brew info.""" | ||
| metadata = PackageMetadata( | ||
| name=data.get("name", ""), | ||
| version=data.get("versions", {}).get("stable", ""), | ||
| description=data.get("desc", ""), | ||
| homepage=data.get("homepage", ""), | ||
| ) | ||
|
|
||
| # Dependencies | ||
| deps = data.get("dependencies", []) | ||
| if isinstance(deps, list): | ||
| metadata.depends.extend(deps) | ||
|
|
||
| # Conflicts | ||
| conflicts = data.get("conflicts_with", []) | ||
| if isinstance(conflicts, list): | ||
| metadata.conflicts.extend(conflicts) | ||
|
|
||
| return metadata | ||
|
|
||
| def parse(self, raw: str) -> PackageMetadata | None: | ||
| """Разобрать JSON вывод brew info --json=v2.""" | ||
| if not raw.strip(): | ||
| return None | ||
| try: | ||
| import json | ||
|
|
||
| data = json.loads(raw) | ||
| if isinstance(data, list) and len(data) > 0: | ||
| return self._parse_brew_json(data[0]) | ||
| elif isinstance(data, dict): | ||
| return self._parse_brew_json(data) | ||
| except json.JSONDecodeError: | ||
| pass | ||
| return None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the
parse()API description.NpmMetadataAdapter.parse()parses an npm-view package object. A package-lock root object has no packagename, so this method returnsNone. State thatparse_lockfile()handles package-lock v2 and v3 files.🤖 Prompt for AI Agents