Skip to content

Support dependency analysis using uv.lock - #332

Closed
woocheol-lge wants to merge 1 commit into
mainfrom
u_test
Closed

Support dependency analysis using uv.lock#332
woocheol-lge wants to merge 1 commit into
mainfrom
u_test

Conversation

@woocheol-lge

@woocheol-lge woocheol-lge commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added support for analyzing Python dependencies defined in uv.lock and pyproject.toml.
    • Dependency reports now include dependency relationships, optional dependencies, environment-specific dependencies, and root-package details.
    • License information is collected from additional installed-package metadata and normalized for more consistent reporting.
  • Bug Fixes

    • Improved dependency analysis reliability by falling back to virtual-environment inspection when lockfile processing fails.
    • Enhanced handling of local packages and direct dependencies in generated OSS reports.

@woocheol-lge woocheol-lge self-assigned this Aug 4, 2026
@woocheol-lge woocheol-lge added the chore [PR/Issue] Refactoring, maintenance the code label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@woocheol-lge, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a2f1a285-67ac-4075-9979-ec4816f43de0

📥 Commits

Reviewing files that changed from the base of the PR and between 6d082bc and cac683c.

📒 Files selected for processing (2)
  • src/fosslight_dependency/constant.py
  • src/fosslight_dependency/package_manager/Pypi.py
📝 Walkthrough

Walkthrough

The PyPI manager now recognizes uv.lock, parses its dependency graph, retrieves package metadata, writes analysis inputs, and integrates direct, transitive, root, and relation data into OSS parsing.

Changes

uv.lock PyPI support

Layer / File(s) Summary
Manifest and dependency discovery
src/fosslight_dependency/constant.py, src/fosslight_dependency/package_manager/Pypi.py
The PyPI manager recognizes uv.lock, parses project dependencies and markers, and reads license data from installed package metadata.
Lockfile graph construction
src/fosslight_dependency/package_manager/Pypi.py
The manager normalizes lockfile packages, filters extras and markers, builds dependency relations, and infers traversal roots.
Package metadata enrichment
src/fosslight_dependency/package_manager/Pypi.py
The manager creates package entries, downloads wheel metadata, and merges PyPI JSON and wheel license information.
Plugin and OSS integration
src/fosslight_dependency/package_manager/Pypi.py
run_plugin prepares uv.lock analysis before virtualenv fallback. OSS parsing records direct and transitive relations, and direct-dependency parsing bypasses pipdeptree for uv.lock.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant run_plugin
  participant prepare_uv_lock
  participant uv.lock
  participant PyPI_metadata
  participant OSS_parser
  run_plugin->>prepare_uv_lock: prepare uv.lock analysis
  prepare_uv_lock->>uv.lock: parse lockfile
  prepare_uv_lock->>PyPI_metadata: fetch package metadata
  PyPI_metadata-->>prepare_uv_lock: return normalized metadata
  prepare_uv_lock->>OSS_parser: write installed-package input and relations
  run_plugin->>run_plugin: fall back to virtualenv inspection on failure
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding dependency analysis support for uv.lock.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch u_test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/fosslight_dependency/package_manager/Pypi.py (7)

659-678: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Register the path that was written, not the relative name.

Line 675 writes to tmp_file_path, which is joined with self.input_dir when self.tmp_file_name is relative. Line 673 registers the relative self.tmp_file_name. The consumer resolves that name against the current working directory. The two agree only while the working directory equals self.input_dir. __del__ at Lines 54-55 also removes self.tmp_file_name relative to the working directory, so the written file can survive if the directories differ.

Register tmp_file_path to remove the dependency on the working directory.

♻️ Proposed refactor
-        if self.tmp_file_name not in self.input_package_list_file:
-            self.append_input_package_list_file(self.tmp_file_name)
+        if tmp_file_path not in self.input_package_list_file:
+            self.append_input_package_list_file(tmp_file_path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 659 - 678,
Update _write_dependency_input_file to register tmp_file_path with
append_input_package_list_file instead of self.tmp_file_name, ensuring the
recorded path matches the file written for both absolute and input_dir-relative
names and is consistently removable by __del__.

409-413: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Iterate values only.

package_name is unused in this loop, which Ruff reports as B007.

♻️ Proposed refactor
-        for package_name, package_info in package_map.items():
+        for package_info in package_map.values():
             for dependency_info in package_info.get('dependencies', []) or []:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 409 - 413,
Update the outer loop over package_map in the dependency-counting logic to
iterate over package info values only, removing the unused package_name binding
while preserving the existing dependency_info iteration and
incoming_dependency_counts updates.

Source: Linters/SAST tools


208-215: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the Poetry fallback loop.

dependency_spec is never used, which Ruff reports as B007. Also 'build-system' is never a key under [tool.poetry].dependencies, so that part of the check is dead.

♻️ Proposed refactor
             if isinstance(poetry_dependencies, dict):
-                for dependency_name, dependency_spec in poetry_dependencies.items():
-                    if dependency_name in {'python', 'build-system'}:
+                for dependency_name in poetry_dependencies:
+                    if dependency_name == 'python':
                         continue
                     root_packages.append(self._normalize_package_name(dependency_name))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 208 - 215, In
the Poetry fallback within the root-package extraction logic, simplify the
dependencies loop to avoid binding the unused dependency_spec value and remove
the dead build-system exclusion; continue excluding only the python key and
normalizing each remaining dependency name via _normalize_package_name.

Source: Linters/SAST tools


954-963: 🚀 Performance & Scalability | 🔵 Trivial

Consider bounding the metadata fetch cost.

This loop performs up to two sequential HTTP requests per package: the PyPI JSON request and the wheel download. A lock file with several hundred packages therefore issues several hundred round trips in series, with no progress output. A user cannot tell whether the scan is stalled.

Two operational improvements apply. Log progress at intervals, for example every 25 packages. Fetch the wheel only when the PyPI JSON metadata lacks a usable license value, which removes most downloads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 954 - 963,
Update the selected_packages loop around _fetch_pypi_metadata to reduce
metadata-fetch cost: pass wheel URLs only when the PyPI JSON metadata lacks a
usable license value, avoiding unnecessary wheel downloads, and emit progress
logging at a regular interval such as every 25 packages so long scans show
activity.

884-895: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the redundant except tuple.

Exception is the last entry, and it already covers urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, and TimeoutError. urllib.error.HTTPError is also a subclass of URLError. Catch Exception alone.

♻️ Proposed refactor
-        except (
-            urllib.error.URLError,
-            urllib.error.HTTPError,
-            json.JSONDecodeError,
-            TimeoutError,
-            Exception,
-        ) as e:
+        except Exception as e:

Then remove the urllib.error import if nothing else uses it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 884 - 895, In
the exception handler surrounding the PyPI metadata fetch, replace the redundant
exception tuple with a catch for Exception alone while preserving the existing
debug logging and fallback data assignment. After updating the handler, remove
the urllib.error import if no other references remain in Pypi.py.

Source: Linters/SAST tools


983-998: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Parse pyproject.toml once.

_get_pyproject_root_packages at Lines 182-184 and this method both open and parse the same pyproject.toml. Extract a single cached parse helper and read the project name and the dependency list from its result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 983 - 998,
Extract a cached helper shared by _get_pyproject_root_packages and
_get_pyproject_project_name that opens and parses pyproject.toml once, returning
the parsed data or the existing empty fallback on missing or invalid files.
Update both methods to read their respective project name and dependency fields
from this cached result, eliminating duplicate file parsing while preserving
current behavior.

707-739: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

license_classifiers is computed but never used.

Both resolvers build license_classifiers, and _merge_uv_license_metadata normalizes it at Lines 818-822. _fetch_pypi_metadata does not include the key in its return value at Lines 926-934, and _build_installed_package_entry does not carry it. parse_oss_information derives the license classifier itself from metadata['classifier'] at Lines 1239-1250.

Remove the field from all three methods, or return it and use it, so the classifier logic exists in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 707 - 739,
Remove the unused license_classifiers computation and returned field from the
resolver paths, including _fetch_pypi_metadata and
_build_installed_package_entry, and remove its normalization in
_merge_uv_license_metadata. Keep parse_oss_information as the single location
that derives license classifiers from metadata['classifier'].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fosslight_dependency/package_manager/Pypi.py`:
- Around line 850-855: The wheel handling in the relevant resolver method must
stop buffering the full HTTP response in memory. Introduce a defined maximum
wheel size, reject or skip responses whose Content-Length exceeds it, stream
smaller/unknown-size responses into a temporary file while enforcing the same
limit, and open that file with zipfile.ZipFile to read only the
*.dist-info/METADATA member; ensure temporary files are cleaned up and oversized
wheels are skipped without breaking _prepare_uv_lock_direct.
- Around line 462-468: Update the empty-package handling in the UV lock
processing flow to return the failure signal expected by run_plugin rather than
treating an empty package list as a successful empty report. Ensure
_prepare_uv_lock_direct propagates this result so run_plugin reaches its
existing virtualenv inspection fallback when uv_lock_data contains no package
entries.
- Around line 159-170: Update _extract_license_from_metadata to reuse
_resolve_core_metadata_license_metadata for parsing core metadata, including
License-Expression and License, instead of manually scanning only License:.
Return the resolved, validated license without falling back to the raw value
when check_UNKNOWN rejects it, while preserving the empty result when no usable
license is found.
- Around line 19-25: Update the project dependency declarations for the Pypi
module to explicitly include both tomli and packaging as runtime dependencies,
while preserving the existing tomllib fallback and packaging imports in Pypi.py.
- Around line 266-277: Update _build_uv_lock_package_map to group all valid
_build_uv_lock_package_info results by normalized package name instead of
overwriting duplicates. For duplicate groups, evaluate each entry’s resolution
markers against the current environment, select the applicable entry, and warn
when multiple entries remain applicable; preserve single-entry behavior and
ensure unresolved groups do not silently select the last entry.
- Around line 1003-1013: Track direct uv.lock processing with a dedicated class
attribute uv_lock_used initialized to False beside pip_activate_cmd, set it only
when _prepare_uv_lock_direct succeeds, and clear or preserve it appropriately
for fallback. Update create_virtualenv to exclude uv.lock from generated install
commands, including when the manifest list is populated from
SUPPORT_PACKAGE[PYPI], and update parse_direct_dependencies to branch on
uv_lock_used rather than checking self.manifest_file_name for uv.lock so
fallback scanning populates direct_dep_list and relation_tree normally.

---

Nitpick comments:
In `@src/fosslight_dependency/package_manager/Pypi.py`:
- Around line 659-678: Update _write_dependency_input_file to register
tmp_file_path with append_input_package_list_file instead of self.tmp_file_name,
ensuring the recorded path matches the file written for both absolute and
input_dir-relative names and is consistently removable by __del__.
- Around line 409-413: Update the outer loop over package_map in the
dependency-counting logic to iterate over package info values only, removing the
unused package_name binding while preserving the existing dependency_info
iteration and incoming_dependency_counts updates.
- Around line 208-215: In the Poetry fallback within the root-package extraction
logic, simplify the dependencies loop to avoid binding the unused
dependency_spec value and remove the dead build-system exclusion; continue
excluding only the python key and normalizing each remaining dependency name via
_normalize_package_name.
- Around line 954-963: Update the selected_packages loop around
_fetch_pypi_metadata to reduce metadata-fetch cost: pass wheel URLs only when
the PyPI JSON metadata lacks a usable license value, avoiding unnecessary wheel
downloads, and emit progress logging at a regular interval such as every 25
packages so long scans show activity.
- Around line 884-895: In the exception handler surrounding the PyPI metadata
fetch, replace the redundant exception tuple with a catch for Exception alone
while preserving the existing debug logging and fallback data assignment. After
updating the handler, remove the urllib.error import if no other references
remain in Pypi.py.
- Around line 983-998: Extract a cached helper shared by
_get_pyproject_root_packages and _get_pyproject_project_name that opens and
parses pyproject.toml once, returning the parsed data or the existing empty
fallback on missing or invalid files. Update both methods to read their
respective project name and dependency fields from this cached result,
eliminating duplicate file parsing while preserving current behavior.
- Around line 707-739: Remove the unused license_classifiers computation and
returned field from the resolver paths, including _fetch_pypi_metadata and
_build_installed_package_entry, and remove its normalization in
_merge_uv_license_metadata. Keep parse_oss_information as the single location
that derives license classifiers from metadata['classifier'].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3fd0eba1-7b10-4573-a9cd-1b0dce936734

📥 Commits

Reviewing files that changed from the base of the PR and between cd7a025 and 6d082bc.

📒 Files selected for processing (2)
  • src/fosslight_dependency/constant.py
  • src/fosslight_dependency/package_manager/Pypi.py

Comment on lines +19 to +25
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib

from packaging.requirements import Requirement, InvalidRequirement
from packaging.markers import Marker

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect declared runtime and dependencies.
fd -H -t f -i 'setup.py|setup.cfg|pyproject.toml|requirements.*\.txt|tox.ini|.python-version' --max-depth 2 --exec cat -n {}

# Look for tomli / packaging / python_requires declarations
rg -n -i 'tomli|packaging|python_requires|requires-python|classifiers' --glob '!**/venv/**' -g 'setup.py' -g 'setup.cfg' -g 'pyproject.toml' -g 'requirements*.txt'

Repository: fosslight/fosslight_dependency_scanner

Length of output: 3579


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from pathlib import Path
import re

root = Path.cwd()
toml = root.joinpath('pyproject.toml')
text = toml.read_text(encoding='utf-8')

project = {}
current = None
for line_no, line in enumerate(text.splitlines(), 1):
    if line.strip().startswith('[project'):
        current = 'project'
    elif line.strip().startswith('['):
        current = None
    elif current == 'project':
        m = re.match(r'(\w+)\s*=\s*(.*)', line.strip())
        if m:
            project[m[1]] = m[2].strip()

deps_raw = project.get('dependencies', '')
deps = re.findall(r'"([^"]+)"|\'([^\']+)\'', deps_raw)
deps = [d[0] or d[1] for d in deps]

declared = {d.lower().split('=', 1)[0] for d in deps}
print("python_requires:", project.get('requires-python', ''), to toml)
print("programming classifiers:", sorted([s.strip('"') for s in re.findall(r'"[^"]*Python :: [^"]+"', text)]))
print("declared deps:", deps)
print("declared contains tomli:", "tomli" in declared)
print("declared contains packaging:", "packaging" in declared)

if (
    "requires-python" in text
    and "tomli" not in declared
    and "packaging" not in declared
    and any("Python :: 3.10" in s or "Python :: 3" in s for s in re.findall(r'"[^"]*Python :: [^"]+"', text))
):
    print("MISSING_DEP_DECLARATION: tomli and/or packaging are imported but not declared for supported Python versions.")
PY

# Directly inspect the import block without executing repository code.
sed -n '1,35p' src/fosslight_dependency/package_manager/Pypi.py

Repository: fosslight/fosslight_dependency_scanner

Length of output: 1303


Declare tomli and packaging before importing them.

The package supports Python 3.10, so the tomli fallback runs before Python 3.11’s stdlib tomllib. packaging.requirements and packaging.markers also need an explicit runtime dependency because they are not part of the Python stdlib.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 19 - 25,
Update the project dependency declarations for the Pypi module to explicitly
include both tomli and packaging as runtime dependencies, while preserving the
existing tomllib fallback and packaging imports in Pypi.py.

Comment on lines +159 to +170
def _extract_license_from_metadata(self, metadata_text):
if not metadata_text:
return ''

for line in metadata_text.splitlines():
if not line.startswith('License:'):
continue
license_value = line.split(':', 1)[1].strip()
if not license_value:
continue
return check_UNKNOWN(check_license_name(license_value)) or license_value
return ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not fall back to the raw License value, and also read License-Expression.

Two problems exist at Line 169:

  1. check_UNKNOWN(...) or license_value restores the raw value when check_UNKNOWN rejects it. METADATA files often contain License: UNKNOWN, so the literal string UNKNOWN can reach oss_item.license. The rejection is then meaningless.
  2. The loop matches only License:. Packages that follow PEP 639 set License-Expression and leave License empty, so this returns '' for them.

_resolve_core_metadata_license_metadata already parses License-Expression, License, Classifier, and License-File from the same core-metadata format. Reuse it here instead of duplicating the parse.

🐛 Proposed fix
     def _extract_license_from_metadata(self, metadata_text):
         if not metadata_text:
             return ''
 
-        for line in metadata_text.splitlines():
-            if not line.startswith('License:'):
-                continue
-            license_value = line.split(':', 1)[1].strip()
-            if not license_value:
-                continue
-            return check_UNKNOWN(check_license_name(license_value)) or license_value
-        return ''
+        parsed = self._resolve_core_metadata_license_metadata(metadata_text)
+        license_value = parsed.get('license_expression', '') or parsed.get('license', '')
+        if not license_value:
+            return ''
+        return check_UNKNOWN(check_license_name(license_value)) or ''
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _extract_license_from_metadata(self, metadata_text):
if not metadata_text:
return ''
for line in metadata_text.splitlines():
if not line.startswith('License:'):
continue
license_value = line.split(':', 1)[1].strip()
if not license_value:
continue
return check_UNKNOWN(check_license_name(license_value)) or license_value
return ''
def _extract_license_from_metadata(self, metadata_text):
if not metadata_text:
return ''
parsed = self._resolve_core_metadata_license_metadata(metadata_text)
license_value = parsed.get('license_expression', '') or parsed.get('license', '')
if not license_value:
return ''
return check_UNKNOWN(check_license_name(license_value)) or ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 159 - 170,
Update _extract_license_from_metadata to reuse
_resolve_core_metadata_license_metadata for parsing core metadata, including
License-Expression and License, instead of manually scanning only License:.
Return the resolved, validated license without falling back to the raw value
when check_UNKNOWN rejects it, while preserving the empty result when no usable
license is found.

Comment on lines +266 to +277
def _build_uv_lock_package_map(self, package_entries):
"""Create a normalized package lookup table from uv.lock entries."""
package_map = {}

for package_entry in package_entries:
package_info = self._build_uv_lock_package_info(package_entry)
if not package_info:
continue

package_map[package_info['name']] = package_info

return package_map

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for uv.lock fixtures or tests in the repo to confirm the expected lock shape.
fd -H -t f 'uv.lock'
fd -H -t d -i 'test' --max-depth 2
rg -n 'resolution-markers|resolution_markers' --glob '!**/venv/**'

Repository: fosslight/fosslight_dependency_scanner

Length of output: 702


🏁 Script executed:

#!/bin/bash
set -e
echo "Tracked Pypi files:"
git ls-files | grep -E '(^|/)Pypi\.py$|(^|/)test_pypi/' | head -200
echo
echo "Pypi excerpt:"
wc -l src/fosslight_dependency/package_manager/Pypi.py
sed -n '230,330p' src/fosslight_dependency/package_manager/Pypi.py
echo
echo "Search uv lock related references:"
rg -n "uv\.lock|uv_lock|resolution-markers|resolution_markers|package_entries|_build_uv_lock_package_map|_get_uv_lock" --glob '!**/venv/**' .

Repository: fosslight/fosslight_dependency_scanner

Length of output: 11193


🌐 Web query:

uv.lock resolution-markers same package multiple versions documentation

💡 Result:

In uv, the presence of multiple versions of the same package within a single uv.lock file is a deliberate feature of its universal, forking resolver [1][2][3]. Because uv generates a lockfile that is intended to be portable across different platforms, operating systems, and Python versions, it may need to resolve different versions of the same dependency depending on the environment [1][3]. The resolution-markers field in the uv.lock file plays a critical role in this process [2][4]. Key technical points regarding this behavior: Forking Resolver: When the resolver encounters requirements that cannot be satisfied by a single version of a package (e.g., due to different markers or conflicting constraints), it splits the resolution into multiple forks [2]. Each fork represents a distinct branch of the dependency graph tailored to specific environmental conditions [2]. Universal Resolution and Markers: Since a Python environment can only install one version of a package at a time, the lockfile records these multiple versions alongside their corresponding resolution-markers [1][5]. These markers act as conditional guards, ensuring that when you run uv sync, the correct version is selected based on your specific platform or environment markers [1][2][3]. Record Keeping: The resolution-markers field stores the specific markers associated with each fork [2]. This ensures that the resolution remains stable across future updates [2]. If the resolver forks during its process, these markers—including, in some cases, conflict markers—are recorded to prevent the resolver from making unstable or unintended changes when it is invoked again [2][4]. Management: Users can influence how uv balances consistency versus the use of newer versions across these forks by using the --fork-strategy setting (e.g., requires-python or fewest) [1][3]. While seeing multiple versions of the same package in a lockfile may appear counterintuitive, it is a normal consequence of creating a platform-agnostic, universal lockfile [6][3]. The resolution-markers guarantee that, for any valid set of markers in a target environment, the final installed dependency graph remains coherent and free of conflicts [5].

Citations:


🌐 Web query:

GitHub issue uv dependency resolver resolution-marks same package multiple resolved packages

💡 Result:

In uv, seeing multiple versions of the same package in a lockfile is a deliberate feature of its forking resolver, not necessarily a bug [1][2]. Because standard Python environments only permit one version of a package at a time, uv uses a forking strategy to handle requirements that diverge based on environment markers (e.g., different platforms, Python versions, or enabled extras) [1][2]. When the resolver encounters multiple, potentially conflicting requirements, it splits the resolution into "forks" [1]. To ensure stability and allow uv to reconstruct the same environment across different machines, these forks—including the specific markers that trigger them—are serialized into the uv.lock file as resolution-markers [1][3]. Key aspects of this behavior include: * Forking Resolver: Inspired by Poetry, uv's resolver creates virtual packages for different conditions (such as different extras) [1]. When in universal mode, the lockfile may contain multiple entries for the same package name if those versions are needed for different, non-overlapping environments [4][2]. * Resolution-Markers: These markers in the lockfile explicitly define which environments (e.g., Python version, platform, or extra) apply to each resolved version [1][5]. This ensures that when a user syncs the project, uv knows exactly which distribution to install based on the current environment [4]. * Conflicting Extras: If you are seeing multiple versions of a package when you expect only one, it is often due to how extras or platform markers are being evaluated [6][7]. Users can sometimes resolve undesirable duplication by explicitly defining conflicting extras in their pyproject.toml (under [tool.uv] conflicts = [...]), which helps the resolver understand that certain combinations of dependencies are mutually exclusive and should not trigger separate, simultaneous forks [8][9]. * Troubleshooting: If you encounter issues where uv is resolving multiple versions unexpectedly, ensure your pyproject.toml correctly defines [tool.uv] conflicts for mutually exclusive extras [8][9]. In some cases, unexpected duplication has been identified as a bug in the resolver's management of fork markers, which the uv team addresses through ongoing patches [6]. In summary, resolution-markers are the mechanism uv uses to track which versions of a package are valid for which environments, allowing it to maintain a stable, multi-platform, or multi-environment lockfile [1][2].

Citations:


Handle multiple uv.lock entries for the same normalized package name.

package_map[package_info['name']] = package_info keeps only the last entry when uv.lock contains multiple [[package]] entries for the same name. Universal lockfiles can do this with different resolution-markers for different platforms, Python versions, or extras, so duplicate names can silently drop valid resolvable versions and report the wrong resolved version. Group entries by normalized name, select the entry whose marker evaluates in the current environment, and warn when several entries remain for the same package name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 266 - 277,
Update _build_uv_lock_package_map to group all valid _build_uv_lock_package_info
results by normalized package name instead of overwriting duplicates. For
duplicate groups, evaluate each entry’s resolution markers against the current
environment, select the applicable entry, and warn when multiple entries remain
applicable; preserve single-entry behavior and ensure unresolved groups do not
silently select the last entry.

Comment on lines +462 to +468
package_entries = (
uv_lock_data.get('package', [])
if uv_lock_data
else []
)
if not package_entries:
return {}, []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

An empty package list produces a silent empty report instead of a fallback.

If uv.lock parses but contains no [[package]] entries, this returns ({}, []). _prepare_uv_lock_direct then iterates nothing, writes {"installed": []} at Line 979, and returns True. run_plugin sees success at Line 1013 and returns immediately, so it never falls back to virtualenv inspection. The scan reports zero dependencies and no warning.

Return a failure signal for this case so the existing fallback at Lines 1006-1011 runs.

🐛 Proposed fix
         if not package_entries:
+            logger.warning('No package entries found in uv.lock.')
             return {}, []

And in _prepare_uv_lock_direct:

         package_map, selected_packages = self._build_uv_lock_metadata(
             uv_lock_data
         )
+        if not selected_packages:
+            logger.warning('uv.lock contains no analyzable packages.')
+            return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 462 - 468,
Update the empty-package handling in the UV lock processing flow to return the
failure signal expected by run_plugin rather than treating an empty package list
as a successful empty report. Ensure _prepare_uv_lock_direct propagates this
result so run_plugin reaches its existing virtualenv inspection fallback when
uv_lock_data contains no package entries.

Comment on lines +850 to +855
with zipfile.ZipFile(io.BytesIO(wheel_bytes)) as wheel_archive:
for archive_name in wheel_archive.namelist():
if not archive_name.endswith('.dist-info/METADATA'):
continue
metadata_text = wheel_archive.read(archive_name).decode('utf-8', 'ignore')
return self._resolve_core_metadata_license_metadata(metadata_text)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cap the wheel download size and avoid loading the whole archive into memory.

Line 848 reads the entire response into memory, and Line 850 wraps those bytes in zipfile.ZipFile. Wheels for packages such as torch or nvidia-* exceed one gigabyte. _prepare_uv_lock_direct calls this method once per selected package in sequence, so a lock file with many large wheels makes the scanner allocate each archive in full. The 10-second timeout bounds the duration of a single read but not the transferred size.

Read the archive to a temporary file with a size limit, and skip the wheel when Content-Length exceeds that limit. Only the *.dist-info/METADATA member is needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_dependency/package_manager/Pypi.py` around lines 850 - 855, The
wheel handling in the relevant resolver method must stop buffering the full HTTP
response in memory. Introduce a defined maximum wheel size, reject or skip
responses whose Content-Length exceeds it, stream smaller/unknown-size responses
into a temporary file while enforcing the same limit, and open that file with
zipfile.ZipFile to read only the *.dist-info/METADATA member; ensure temporary
files are cleaned up and oversized wheels are skipped without breaking
_prepare_uv_lock_direct.

Comment thread src/fosslight_dependency/package_manager/Pypi.py
Signed-off-by: woocheol <jayden6659@gmail.com>

@dd-jy dd-jy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@woocheol-lge
coderabbit 리뷰 코멘트 수정해주시기 바랍니다.

@woocheol-lge
woocheol-lge deleted the u_test branch August 25, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore [PR/Issue] Refactoring, maintenance the code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants