Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions uv/lib/dependabot/uv/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module Uv
class FileFetcher < Dependabot::Python::SharedFileFetcher
extend T::Sig

ECOSYSTEM_SPECIFIC_FILES = T.let(%w(uv.lock).freeze, T::Array[String])
ECOSYSTEM_SPECIFIC_FILES = T.let(%w(uv.lock uv.toml).freeze, T::Array[String])

REQUIREMENT_FILE_PATTERNS = T.let(
{
Expand All @@ -34,13 +34,12 @@ class FileFetcher < Dependabot::Python::SharedFileFetcher

sig { override.returns(T::Array[String]) }
def self.ecosystem_specific_required_files
# uv.lock is not a standalone required file - it requires pyproject.toml
[]
%w(uv.toml)
end

sig { override.returns(String) }
def self.required_files_message
"Repo must contain a requirements.txt, uv.lock, requirements.in, or pyproject.toml"
"Repo must contain a requirements.txt, uv.lock, uv.toml, requirements.in, or pyproject.toml"
end
Comment on lines 35 to 43
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

required_files_message states that a repo may contain uv.lock to be considered supported, but Dependabot::Python::SharedFileFetcher.required_files_in? does not treat uv.lock as a satisfying required file for the uv ecosystem (only .txt/.in, pyproject.toml, requirements, or ecosystem_specific_required_files, which is currently just uv.toml). This can mislead users when they hit the "missing required files" error.

Consider either (a) removing uv.lock from the message / clarifying it requires pyproject.toml, or (b) adding uv.lock to ecosystem_specific_required_files if it should truly be sufficient on its own.

Copilot uses AI. Check for mistakes.

private
Expand All @@ -51,6 +50,7 @@ def ecosystem_specific_files
files += readme_files
files += license_files
files += uv_lock_files
files += uv_toml_files
files += workspace_member_files
files += version_source_files
files
Expand Down Expand Up @@ -261,6 +261,12 @@ def child_uv_lock_files
child_requirement_files.select { |f| f.name.end_with?("uv.lock") }
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def uv_toml_files
file = fetch_file_if_present("uv.toml")
file ? [file] : []
end

sig { override.returns(T::Array[Dependabot::DependencyFile]) }
def req_txt_and_in_files
return @req_txt_and_in_files if @req_txt_and_in_files
Expand Down
15 changes: 15 additions & 0 deletions uv/lib/dependabot/uv/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class FileParser < Dependabot::FileParsers::Base

require_relative "file_parser/pyproject_files_parser"
require_relative "file_parser/python_requirement_parser"
require_relative "file_parser/uv_version_parser"

DEPENDENCY_GROUP_KEYS = T.let(
[
Expand Down Expand Up @@ -55,6 +56,7 @@ def parse
dependency_set += pyproject_file_dependencies if pyproject
dependency_set += uv_lock_file_dependencies
dependency_set += requirement_dependencies if requirement_files.any?
dependency_set += uv_version_dependencies

dependency_set.dependencies
end
Expand Down Expand Up @@ -217,6 +219,13 @@ def uv_lock_file_dependencies
dependency_set
end

sig { returns(DependencySet) }
def uv_version_dependencies
UvVersionParser.new(
dependency_files: dependency_files
).dependency_set
end

sig { returns(DependencySet) }
def pyproject_file_dependencies
@pyproject_file_dependencies ||= T.let(
Expand Down Expand Up @@ -409,6 +418,7 @@ def check_required_files
filenames = dependency_files.map(&:name)
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
return if pyproject
return if uv_toml_file

raise "Missing required files!"
end
Expand All @@ -418,6 +428,11 @@ def pyproject
@pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(DependencyFile))
end

sig { returns(T.nilable(DependencyFile)) }
def uv_toml_file
@uv_toml_file ||= T.let(dependency_files.find { |f| f.name == "uv.toml" }, T.nilable(DependencyFile))
end

sig { returns(T::Array[DependencyFile]) }
def requirements_in_files
dependency_files.select { |f| f.name.end_with?(".in") }
Expand Down
114 changes: 114 additions & 0 deletions uv/lib/dependabot/uv/file_parser/uv_version_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# typed: strict
# frozen_string_literal: true

require "toml-rb"
require "dependabot/dependency"
require "dependabot/file_parsers/base/dependency_set"
require "dependabot/uv/file_parser"
require "dependabot/uv/requirement"

module Dependabot
module Uv
class FileParser
# Parses the `required-version` field from `uv.toml` and
# `[tool.uv]` in `pyproject.toml` to track the pinned uv tool version.
class UvVersionParser
extend T::Sig

UV_TOOL_DEP_NAME = "uv:required-version"

sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
def initialize(dependency_files:)
@dependency_files = dependency_files
end

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def dependency_set
deps = Dependabot::FileParsers::Base::DependencySet.new

uv_toml_dep = parse_from_uv_toml
deps << uv_toml_dep if uv_toml_dep

pyproject_dep = parse_from_pyproject
deps << pyproject_dep if pyproject_dep

deps
end

private

sig { returns(T::Array[Dependabot::DependencyFile]) }
attr_reader :dependency_files

sig { returns(T.nilable(Dependabot::Dependency)) }
def parse_from_uv_toml
file = uv_toml_file
return unless file

parsed = TomlRB.parse(T.must(file.content))
required_version = parsed["required-version"]
return unless required_version.is_a?(String) && !required_version.empty?

build_dependency(required_version, file.name)
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
nil
end

sig { returns(T.nilable(Dependabot::Dependency)) }
def parse_from_pyproject
return unless pyproject

parsed = TomlRB.parse(T.must(T.must(pyproject).content))
required_version = parsed.dig("tool", "uv", "required-version")
return unless required_version.is_a?(String) && !required_version.empty?

build_dependency(required_version, T.must(pyproject).name)
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
nil
end

sig { params(requirement_string: String, filename: String).returns(Dependabot::Dependency) }
def build_dependency(requirement_string, filename)
Dependabot::Dependency.new(
name: UV_TOOL_DEP_NAME,
version: extract_exact_version(requirement_string),
requirements: [{
requirement: requirement_string,
file: filename,
source: nil,
groups: ["uv-required-version"]
}],
package_manager: "uv"
)
end

sig { params(requirement_string: String).returns(T.nilable(String)) }
def extract_exact_version(requirement_string)
reqs = Requirement.requirements_array(requirement_string)
return nil unless reqs.length == 1

req = T.must(reqs.first)
return nil unless req.exact?

req.requirements.first&.last&.to_s
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
def uv_toml_file
@uv_toml_file ||= T.let(
dependency_files.find { |f| f.name == "uv.toml" },
T.nilable(Dependabot::DependencyFile)
)
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
def pyproject
@pyproject ||= T.let(
dependency_files.find { |f| f.name == "pyproject.toml" },
T.nilable(Dependabot::DependencyFile)
)
end
end
end
end
end
33 changes: 30 additions & 3 deletions uv/lib/dependabot/uv/file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ class FileUpdater < Dependabot::FileUpdaters::Base
require_relative "file_updater/compile_file_updater"
require_relative "file_updater/lock_file_updater"
require_relative "file_updater/requirement_file_updater"
require_relative "file_updater/uv_version_file_updater"

sig { override.returns(T::Array[DependencyFile]) }
def updated_dependency_files
updated_files = updated_pip_compile_based_files
updated_files += updated_uv_lock_files
updated_files += updated_uv_version_files

if updated_files.none? ||
updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
Expand All @@ -38,10 +40,17 @@ def subdependency_resolver
:pip_compile if pip_compile_files.any?
end

sig { returns(T::Array[Dependabot::Dependency]) }
def package_dependencies
dependencies.reject { |dep| dep.name == "uv:required-version" }
end

sig { returns(T::Array[DependencyFile]) }
def updated_pip_compile_based_files
return [] if package_dependencies.empty?

CompileFileUpdater.new(
dependencies: dependencies,
dependencies: package_dependencies,
dependency_files: dependency_files,
credentials: credentials,
index_urls: pip_compile_index_urls
Expand All @@ -50,8 +59,10 @@ def updated_pip_compile_based_files

sig { returns(T::Array[DependencyFile]) }
def updated_requirement_based_files
return [] if package_dependencies.empty?

RequirementFileUpdater.new(
dependencies: dependencies,
dependencies: package_dependencies,
dependency_files: dependency_files,
credentials: credentials,
index_urls: pip_compile_index_urls
Expand All @@ -60,15 +71,25 @@ def updated_requirement_based_files

sig { returns(T::Array[DependencyFile]) }
def updated_uv_lock_files
return [] if package_dependencies.empty?

LockFileUpdater.new(
dependencies: dependencies,
dependencies: package_dependencies,
dependency_files: dependency_files,
credentials: credentials,
index_urls: pip_compile_index_urls,
repo_contents_path: repo_contents_path
).updated_dependency_files
end

sig { returns(T::Array[DependencyFile]) }
def updated_uv_version_files
UvVersionFileUpdater.new(
dependencies: dependencies,
dependency_files: dependency_files
).updated_dependency_files
end

sig { returns(T::Array[T.nilable(String)]) }
def pip_compile_index_urls
if credentials.any?(&:replaces_base?)
Expand All @@ -86,6 +107,7 @@ def check_required_files
filenames = dependency_files.map(&:name)
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
return if pyproject
return if uv_toml

raise "Missing required files!"
end
Expand All @@ -95,6 +117,11 @@ def pyproject
@pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile))
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
def uv_toml
@uv_toml ||= T.let(get_original_file("uv.toml"), T.nilable(Dependabot::DependencyFile))
end

sig { returns(T::Array[DependencyFile]) }
def pip_compile_files
@pip_compile_files ||= T.let(
Expand Down
Loading
Loading