Support dependency analysis using uv.lock - #332
Conversation
|
Warning Review limit reached
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PyPI manager now recognizes Changesuv.lock PyPI support
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (7)
src/fosslight_dependency/package_manager/Pypi.py (7)
659-678: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRegister the path that was written, not the relative name.
Line 675 writes to
tmp_file_path, which is joined withself.input_dirwhenself.tmp_file_nameis relative. Line 673 registers the relativeself.tmp_file_name. The consumer resolves that name against the current working directory. The two agree only while the working directory equalsself.input_dir.__del__at Lines 54-55 also removesself.tmp_file_namerelative to the working directory, so the written file can survive if the directories differ.Register
tmp_file_pathto 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 valueIterate values only.
package_nameis 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 valueSimplify the Poetry fallback loop.
dependency_specis 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 | 🔵 TrivialConsider 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 valueSimplify the redundant
excepttuple.
Exceptionis the last entry, and it already coversurllib.error.URLError,urllib.error.HTTPError,json.JSONDecodeError, andTimeoutError.urllib.error.HTTPErroris also a subclass ofURLError. CatchExceptionalone.♻️ Proposed refactor
- except ( - urllib.error.URLError, - urllib.error.HTTPError, - json.JSONDecodeError, - TimeoutError, - Exception, - ) as e: + except Exception as e:Then remove the
urllib.errorimport 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 valueParse
pyproject.tomlonce.
_get_pyproject_root_packagesat Lines 182-184 and this method both open and parse the samepyproject.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_classifiersis computed but never used.Both resolvers build
license_classifiers, and_merge_uv_license_metadatanormalizes it at Lines 818-822._fetch_pypi_metadatadoes not include the key in its return value at Lines 926-934, and_build_installed_package_entrydoes not carry it.parse_oss_informationderives the license classifier itself frommetadata['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
📒 Files selected for processing (2)
src/fosslight_dependency/constant.pysrc/fosslight_dependency/package_manager/Pypi.py
| try: | ||
| import tomllib | ||
| except ModuleNotFoundError: # pragma: no cover | ||
| import tomli as tomllib | ||
|
|
||
| from packaging.requirements import Requirement, InvalidRequirement | ||
| from packaging.markers import Marker |
There was a problem hiding this comment.
📐 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.pyRepository: 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.
| 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 '' |
There was a problem hiding this comment.
🎯 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:
check_UNKNOWN(...) or license_valuerestores the raw value whencheck_UNKNOWNrejects it.METADATAfiles often containLicense: UNKNOWN, so the literal stringUNKNOWNcan reachoss_item.license. The rejection is then meaningless.- The loop matches only
License:. Packages that follow PEP 639 setLicense-Expressionand leaveLicenseempty, 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.
| 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.
| 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 |
There was a problem hiding this comment.
🗄️ 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:
- 1: https://docs.astral.sh/uv/concepts/resolution/index.md
- 2: https://docs.astral.sh/uv/reference/internals/resolver/
- 3: https://docs.astral.sh/uv/concepts/resolution/
- 4: uv-resolver: include conflict markers in fork markers astral-sh/uv#10818
- 5: https://github.com/astral-sh/uv/blob/8ee34679/docs/reference/internals/metadata.md
- 6: constraint-dependencies ignored for transitive dep, leading to multiple versions of package in uv.lock astral-sh/uv#18337
🌐 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:
- 1: https://docs.astral.sh/uv/reference/internals/resolver/
- 2: https://docs.astral.sh/uv/concepts/resolution/
- 3: https://github.com/astral-sh/uv/blob/main/docs/reference/internals/resolver.md
- 4: https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/resolver/mod.rs
- 5: Discard insufficient fork markers astral-sh/uv#10682
- 6: multiple versions of the same package can be installed in some cases when using conflicting extras astral-sh/uv#10985
- 7: constraint-dependencies ignored for transitive dep, leading to multiple versions of package in uv.lock astral-sh/uv#18337
- 8: Duplicate packages with conflicting extras astral-sh/uv#11133
- 9: uv lock does not resolve dependencies with multiple indexes and
extramarkers as expected astral-sh/uv#9086
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.
| package_entries = ( | ||
| uv_lock_data.get('package', []) | ||
| if uv_lock_data | ||
| else [] | ||
| ) | ||
| if not package_entries: | ||
| return {}, [] |
There was a problem hiding this comment.
🩺 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.
| 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) |
There was a problem hiding this comment.
🩺 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.
Signed-off-by: woocheol <jayden6659@gmail.com>
dd-jy
left a comment
There was a problem hiding this comment.
@woocheol-lge
coderabbit 리뷰 코멘트 수정해주시기 바랍니다.
Summary by CodeRabbit
New Features
uv.lockandpyproject.toml.Bug Fixes