dependabot-uv 0.300.0 → 0.301.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,104 +19,124 @@ module Dependabot
19
19
  @lockfile = lockfile
20
20
  end
21
21
 
22
- # For hosted Dependabot token will be nil since the credentials aren't present.
23
- # This is for those running Dependabot themselves and for dry-run.
24
- def add_auth_env_vars(credentials)
25
- TomlRB.parse(@pyproject_content).dig("tool", "poetry", "source")&.each do |source|
26
- cred = credentials&.find { |c| c["index-url"] == source["url"] }
27
- next unless cred
28
-
29
- token = cred.fetch("token", nil)
30
- next unless token && token.count(":") == 1
31
-
32
- arr = token.split(":")
33
- # https://python-poetry.org/docs/configuration/#using-environment-variables
34
- name = source["name"]&.upcase&.gsub(/\W/, "_")
35
- ENV["POETRY_HTTP_BASIC_#{name}_USERNAME"] = arr[0]
36
- ENV["POETRY_HTTP_BASIC_#{name}_PASSWORD"] = arr[1]
22
+ def freeze_top_level_dependencies_except(dependencies_to_update)
23
+ return @pyproject_content unless lockfile
24
+
25
+ pyproject_object = TomlRB.parse(@pyproject_content)
26
+ deps_to_update_names = dependencies_to_update.map(&:name)
27
+
28
+ if pyproject_object["project"]&.key?("dependencies")
29
+ locked_deps = parsed_lockfile_dependencies || {}
30
+
31
+ pyproject_object["project"]["dependencies"] =
32
+ pyproject_object["project"]["dependencies"].map do |dep_string|
33
+ freeze_dependency(dep_string, deps_to_update_names, locked_deps)
34
+ end
37
35
  end
36
+
37
+ TomlRB.dump(pyproject_object)
38
38
  end
39
39
 
40
- def update_python_requirement(requirement)
40
+ def update_python_requirement(python_version)
41
+ return @pyproject_content unless python_version
42
+
41
43
  pyproject_object = TomlRB.parse(@pyproject_content)
42
- if (python_specification = pyproject_object.dig("tool", "poetry", "dependencies", "python"))
43
- python_req = Uv::Requirement.new(python_specification)
44
- unless python_req.satisfied_by?(requirement)
45
- pyproject_object["tool"]["poetry"]["dependencies"]["python"] = "~#{requirement}"
46
- end
44
+
45
+ if pyproject_object["project"]&.key?("requires-python")
46
+ pyproject_object["project"]["requires-python"] = ">=#{python_version}"
47
47
  end
48
+
48
49
  TomlRB.dump(pyproject_object)
49
50
  end
50
51
 
51
- def sanitize
52
- # {{ name }} syntax not allowed
53
- pyproject_content
54
- .gsub(/\{\{.*?\}\}/, "something")
55
- .gsub('#{', "{")
56
- end
52
+ def add_auth_env_vars(credentials)
53
+ return unless credentials
57
54
 
58
- # rubocop:disable Metrics/PerceivedComplexity
59
- # rubocop:disable Metrics/AbcSize
60
- def freeze_top_level_dependencies_except(dependencies)
61
- return pyproject_content unless lockfile
62
-
63
- pyproject_object = TomlRB.parse(pyproject_content)
64
- poetry_object = pyproject_object["tool"]["poetry"]
65
- excluded_names = dependencies.map(&:name) + ["python"]
66
-
67
- Dependabot::Uv::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |key|
68
- next unless poetry_object[key]
69
-
70
- source_types = %w(directory file url)
71
- poetry_object.fetch(key).each do |dep_name, _|
72
- next if excluded_names.include?(normalise(dep_name))
73
-
74
- locked_details = locked_details(dep_name)
75
-
76
- next unless (locked_version = locked_details&.fetch("version"))
77
-
78
- next if source_types.include?(locked_details&.dig("source", "type"))
79
-
80
- if locked_details&.dig("source", "type") == "git"
81
- poetry_object[key][dep_name] = {
82
- "git" => locked_details&.dig("source", "url"),
83
- "rev" => locked_details&.dig("source", "reference")
84
- }
85
- subdirectory = locked_details&.dig("source", "subdirectory")
86
- poetry_object[key][dep_name]["subdirectory"] = subdirectory if subdirectory
87
- elsif poetry_object[key][dep_name].is_a?(Hash)
88
- poetry_object[key][dep_name]["version"] = locked_version
89
- elsif poetry_object[key][dep_name].is_a?(Array)
90
- # if it has multiple-constraints, locking to a single version is
91
- # going to result in a bad lockfile, ignore
92
- next
93
- else
94
- poetry_object[key][dep_name] = locked_version
95
- end
96
- end
55
+ credentials.each do |credential|
56
+ next unless credential["type"] == "python_index"
57
+
58
+ token = credential["token"]
59
+ index_url = credential["index-url"]
60
+
61
+ next unless token && index_url
62
+
63
+ # Set environment variables for uv auth
64
+ ENV["UV_INDEX_URL_TOKEN_#{sanitize_env_name(index_url)}"] = token
65
+
66
+ # Also set pip-style credentials for compatibility
67
+ ENV["PIP_INDEX_URL"] ||= "https://#{token}@#{index_url.gsub(%r{^https?://}, '')}"
97
68
  end
69
+ end
98
70
 
99
- TomlRB.dump(pyproject_object)
71
+ def sanitize
72
+ # No special sanitization needed for UV files at this point
73
+ @pyproject_content
100
74
  end
101
- # rubocop:enable Metrics/AbcSize
102
- # rubocop:enable Metrics/PerceivedComplexity
103
75
 
104
76
  private
105
77
 
106
- attr_reader :pyproject_content
107
78
  attr_reader :lockfile
108
79
 
109
- def locked_details(dep_name)
110
- parsed_lockfile.fetch("package")
111
- .find { |d| d["name"] == normalise(dep_name) }
80
+ def parsed_lockfile
81
+ @parsed_lockfile ||= lockfile ? parse_lockfile(lockfile.content) : {}
112
82
  end
113
83
 
114
- def normalise(name)
115
- NameNormaliser.normalise(name)
84
+ def parse_lockfile(content)
85
+ TomlRB.parse(content)
86
+ rescue TomlRB::ParseError
87
+ {} # Return empty hash if parsing fails
116
88
  end
117
89
 
118
- def parsed_lockfile
119
- @parsed_lockfile ||= TomlRB.parse(lockfile.content)
90
+ def parsed_lockfile_dependencies
91
+ return {} unless lockfile
92
+
93
+ deps = {}
94
+ parsed = parsed_lockfile
95
+
96
+ # Handle UV lock format (version 1)
97
+ if parsed["version"] == 1 && parsed["package"].is_a?(Array)
98
+ parsed["package"].each do |pkg|
99
+ next unless pkg["name"] && pkg["version"]
100
+
101
+ deps[pkg["name"]] = { "version" => pkg["version"] }
102
+ end
103
+ # Handle traditional Poetry-style lock format
104
+ elsif parsed["dependencies"]
105
+ deps = parsed["dependencies"]
106
+ end
107
+
108
+ deps
109
+ end
110
+
111
+ def locked_version_for_dep(locked_deps, dep_name)
112
+ locked_deps.each do |name, details|
113
+ next unless Uv::FileParser.normalize_dependency_name(name) == dep_name
114
+ return details["version"] if details.is_a?(Hash) && details["version"]
115
+ end
116
+ nil
117
+ end
118
+
119
+ def sanitize_env_name(url)
120
+ url.gsub(%r{^https?://}, "").gsub(/[^a-zA-Z0-9]/, "_").upcase
121
+ end
122
+
123
+ def freeze_dependency(dep_string, deps_to_update_names, locked_deps)
124
+ package_name = dep_string.split(/[=>~<\[]/).first.strip
125
+ normalized_name = Uv::FileParser.normalize_dependency_name(package_name)
126
+
127
+ return dep_string if deps_to_update_names.include?(normalized_name)
128
+
129
+ version = locked_version_for_dep(locked_deps, normalized_name)
130
+ return dep_string unless version
131
+
132
+ if dep_string.include?("=") || dep_string.include?(">") ||
133
+ dep_string.include?("<") || dep_string.include?("~")
134
+ # Replace version constraint with exact version
135
+ dep_string.sub(/[=>~<\[].*$/, "==#{version}")
136
+ else
137
+ # Simple dependency, just append version
138
+ "#{dep_string}==#{version}"
139
+ end
120
140
  end
121
141
  end
122
142
  end
@@ -13,6 +13,7 @@ module Dependabot
13
13
  extend T::Sig
14
14
 
15
15
  require_relative "file_updater/compile_file_updater"
16
+ require_relative "file_updater/lock_file_updater"
16
17
  require_relative "file_updater/requirement_file_updater"
17
18
 
18
19
  sig { override.returns(T::Array[Regexp]) }
@@ -20,13 +21,15 @@ module Dependabot
20
21
  [
21
22
  /^.*\.txt$/, # Match any .txt files (e.g., requirements.txt) at any level
22
23
  /^.*\.in$/, # Match any .in files at any level
23
- /^.*pyproject\.toml$/ # Match pyproject.toml at any level
24
+ /^.*pyproject\.toml$/, # Match pyproject.toml at any level
25
+ /^.*uv\.lock$/ # Match uv.lock at any level
24
26
  ]
25
27
  end
26
28
 
27
29
  sig { override.returns(T::Array[DependencyFile]) }
28
30
  def updated_dependency_files
29
31
  updated_files = updated_pip_compile_based_files
32
+ updated_files += updated_uv_lock_files
30
33
 
31
34
  if updated_files.none? ||
32
35
  updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
@@ -65,6 +68,16 @@ module Dependabot
65
68
  ).updated_dependency_files
66
69
  end
67
70
 
71
+ sig { returns(T::Array[DependencyFile]) }
72
+ def updated_uv_lock_files
73
+ LockFileUpdater.new(
74
+ dependencies: dependencies,
75
+ dependency_files: dependency_files,
76
+ credentials: credentials,
77
+ index_urls: pip_compile_index_urls
78
+ ).updated_dependency_files
79
+ end
80
+
68
81
  sig { returns(T::Array[String]) }
69
82
  def pip_compile_index_urls
70
83
  if credentials.any?(&:replaces_base?)