dependabot-uv 0.382.0 → 0.383.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f8d39e39ce8e18f7c946928ed3a7b5cd780ce3ae7d8808dad8c96c3b8248e6b
4
- data.tar.gz: 2747bc7341934590fb3c0d1b0af95f788e055ff7845570dc10918179c0a9a26b
3
+ metadata.gz: ec6b2f2a71183f96ba54935778ae0881d4e54de983b0185651871b5582f43527
4
+ data.tar.gz: 6d52e75689a7a2b202deded8bb158f6e882e682a77b03738c6af717ce6a7b37e
5
5
  SHA512:
6
- metadata.gz: 4f6677be24fdf402b476d7018f98ca323491fb6c4ea7ba5eeb4634d0e48b097e98dde9c679b9c1229dcc6343d38600e17d83a2fb931ca3a6918742d8bc88f777
7
- data.tar.gz: f56c52f7b7d3b38d894bfa5452a6c4d68a61cf0ea995f2a26ec75a7d68c4bc4abb7a41a495ba0e699c85597bb8d74b9ddc5ee2413ba009db260102807f716d5c
6
+ metadata.gz: d193574d174b6266342042d95e59ba99b0a89b90ef4878e4735c9dfd27c0fb76be68a54ff85c9d5876e9ebbed85d505909f0cd9f598eb7dae8e443d7fb8d8bc4
7
+ data.tar.gz: fc9b36d7a7482e8642cb8bb6ecc348496972ccb938139c645cf5a96d7f6b0239b8ec1219f0d8cb497bb82eed12fca7336c86febd3f2d1a6aafc3920e985a9c4d
data/helpers/build CHANGED
@@ -15,21 +15,13 @@ cp -r \
15
15
  "$helpers_dir/lib" \
16
16
  "$helpers_dir/run.py" \
17
17
  "$helpers_dir/requirements.txt" \
18
- "$helpers_dir/requirements-3.9.txt" \
19
18
  "$install_dir"
20
19
 
21
20
  cd "$install_dir"
22
21
 
23
22
  python_version=$1
24
- # pip 26.x and several other packages require Python >=3.10.
25
- # Use 3.9-compatible versions for the deprecated Python 3.9 runtime.
26
- if [[ "$python_version" == 3.9.* ]]; then
27
- req_file="requirements-3.9.txt"
28
- else
29
- req_file="requirements.txt"
30
- fi
31
23
 
32
- PYENV_VERSION=$python_version pyenv exec pip3 --disable-pip-version-check install --use-pep517 -r "$req_file"
24
+ PYENV_VERSION=$python_version pyenv exec pip3 --disable-pip-version-check install --use-pep517 -r requirements.txt
33
25
 
34
26
  # Remove the extra objects added during the previous install. Based on
35
27
  # https://github.com/docker-library/python/blob/master/Dockerfile-linux.template
@@ -3,39 +3,55 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
 
6
+ require "dependabot/dependency"
6
7
  require "dependabot/dependency_graphers"
7
8
  require "dependabot/dependency_graphers/base"
8
9
  require "dependabot/uv/file_parser"
9
- require "dependabot/uv/name_normaliser"
10
10
  require "toml-rb"
11
11
 
12
12
  module Dependabot
13
13
  module Uv
14
14
  class DependencyGrapher < Dependabot::DependencyGraphers::Base
15
- UV_LOCK_COMMAND = T.let("pyenv exec uv lock --color never --no-progress && cat uv.lock", String)
16
- UV_TREE_COMMAND = T.let("pyenv exec uv tree -q --color never --no-progress --frozen", String)
17
-
18
- # Used to capture package lines from `uv tree` output.
19
- #
20
- # Example output:
21
- # ├── flask v3.1.3
22
- # │ ├── click v8.3.1
23
- # │ └── jinja2 v3.1.6
24
- # │ └── markupsafe v3.0.3
25
- #
26
- # The `prefix` contains tree-depth segments (`│ ` or ` `) and
27
- # `package` is the dependency name token before the `v<version>` marker.
28
- UV_TREE_LINE_REGEX = T.let(
29
- /^(?<prefix>(?:(?:│ )|(?: ))*)(?:├──|└──)\s(?<package>.+?)\sv[^\s]+(?:\s+\(.*\))?$/,
30
- Regexp
31
- )
15
+ RUNTIME_GROUP = T.let("dependencies", String)
16
+ DEV_GROUP = T.let("dev-dependencies", String)
32
17
 
33
18
  sig { override.returns(Dependabot::DependencyFile) }
34
19
  def relevant_dependency_file
35
- return T.must(uv_lock) if uv_lock
36
- return T.must(pyproject_toml) if pyproject_toml
20
+ uv_lock || raise(DependabotError, "No uv.lock present; uv graphing requires a lockfile.")
21
+ end
22
+
23
+ # uv.lock is guaranteed to be present when graphing runs - the
24
+ # dependabot-api EcosystemFileDetector only routes UV jobs when it sees
25
+ # a uv.lock in the repo. We override prepare! to parse uv.lock directly
26
+ # rather than delegating to FileParser, so the graph reflects only what
27
+ # uv actually resolved (no requirements.txt / pyproject.toml inputs).
28
+ sig { override.void }
29
+ def prepare!
30
+ raise DependabotError, "No uv.lock present; uv graphing requires a lockfile." unless uv_lock
31
+
32
+ parsed = TomlRB.parse(T.must(T.must(uv_lock).content))
33
+ packages = T.cast(parsed.fetch("package", []), T::Array[T.untyped])
34
+ manifest = parsed.fetch("manifest", {})
35
+
36
+ root_names = root_package_names(packages, manifest)
37
+ direct_runtime, direct_dev = direct_dependency_names(packages, root_names)
37
38
 
38
- raise DependabotError, "No uv.lock or pyproject.toml present."
39
+ @dependencies = packages.filter_map do |pkg|
40
+ build_dependency(pkg, root_names, direct_runtime, direct_dev)
41
+ end
42
+ @prepared = true
43
+ rescue DependabotError
44
+ raise
45
+ rescue StandardError => e
46
+ # If uv.lock is unparseable we can't build a graph at all, but we still
47
+ # want the rest of the submission flow to continue (matching the prior
48
+ # behaviour where lockfile parse failures only marked subdependency
49
+ # fetching as errored).
50
+ errored_fetching_subdependencies!
51
+ @subdependency_error = e
52
+ Dependabot.logger.error("Failed to parse uv.lock for graphing: #{e.message}")
53
+ @dependencies = []
54
+ @prepared = true
39
55
  end
40
56
 
41
57
  private
@@ -49,31 +65,11 @@ module Dependabot
49
65
  sig { returns(T::Hash[String, T::Array[String]]) }
50
66
  def package_relationships
51
67
  @package_relationships ||= T.let(
52
- fetch_package_relationships,
68
+ package_relationships_from_lockfile(T.must(T.must(uv_lock).content)),
53
69
  T.nilable(T::Hash[String, T::Array[String]])
54
70
  )
55
71
  end
56
72
 
57
- # See UV tree docs https://docs.astral.sh/uv/reference/cli/#uv-tree
58
- # First try extracting relationships from uv.lock directly. If there is no
59
- # lockfile, generate one in a temporary parsed context and parse that.
60
- # If lockfile parsing fails for any reason, fall back to uv tree output.
61
- sig { returns(T::Hash[String, T::Array[String]]) }
62
- def fetch_package_relationships
63
- return package_relationships_from_lockfile(T.must(T.must(uv_lock).content)) if uv_lock
64
-
65
- begin
66
- Dependabot.logger.info("No uv.lock present, generating ephemeral lockfile for dependency graphing")
67
- generated_lockfile = uv_parser.run_in_parsed_context(UV_LOCK_COMMAND)
68
- return package_relationships_from_lockfile(generated_lockfile)
69
- rescue StandardError => e
70
- Dependabot.logger.warn("Failed to build dependency graph from uv.lock: #{e.message}")
71
- Dependabot.logger.info("Falling back to parsing uv tree output")
72
- end
73
-
74
- package_relationships_from_tree
75
- end
76
-
77
73
  sig { params(lockfile_content: String).returns(T::Hash[String, T::Array[String]]) }
78
74
  def package_relationships_from_lockfile(lockfile_content)
79
75
  lockfile_packages(lockfile_content).each_with_object({}) do |package_data, rels|
@@ -106,19 +102,18 @@ module Dependabot
106
102
  normalised_dependency_name(package_name)
107
103
  end
108
104
 
105
+ # Mirrors uv's `create_dependencies` (crates/uv-resolver/src/lock/export/cyclonedx_json.rs),
106
+ # which chains a package's `dependencies`, `optional-dependencies`, and
107
+ # `dev-dependencies` when building the SBOM dependency graph.
109
108
  sig { params(package_data: T.untyped).returns(T::Array[String]) }
110
109
  def lockfile_child_names(package_data)
111
- dependencies =
112
- if package_data.is_a?(Hash)
113
- T.cast(package_data["dependencies"], T.nilable(T::Array[T.untyped])) || []
114
- else
115
- []
116
- end
117
-
118
- dependencies.filter_map do |dependency|
119
- dependency_name = lockfile_dependency_name(dependency)
120
- normalised_dependency_name(dependency_name) if dependency_name
121
- end
110
+ return [] unless package_data.is_a?(Hash)
111
+
112
+ names = T.let([], T::Array[String])
113
+ collect_dep_names(package_data["dependencies"], names)
114
+ collect_dep_names_from_groups(package_data["optional-dependencies"], names)
115
+ collect_dep_names_from_groups(package_data["dev-dependencies"], names)
116
+ names.map { |name| normalised_dependency_name(name) }.uniq
122
117
  end
123
118
 
124
119
  sig { params(dependency_data: T.untyped).returns(T.nilable(String)) }
@@ -133,32 +128,130 @@ module Dependabot
133
128
  nil
134
129
  end
135
130
 
136
- sig { returns(T::Hash[String, T::Array[String]]) }
137
- def package_relationships_from_tree
138
- relationship_stack = T.let([], T::Array[String])
131
+ # Identifies the workspace member packages whose `dependencies`,
132
+ # `optional-dependencies`, and `dev-dependencies` arrays describe the
133
+ # project's direct deps.
134
+ #
135
+ # Authoritative signal: the `[manifest] members = [...]` array, which uv
136
+ # writes for multi-member workspaces. See
137
+ # https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/lock/mod.rs
138
+ # ("manifest_table.insert(\"members\", ...)" and the workspace-member
139
+ # lookup `self.members().contains(&package.id.name)`).
140
+ #
141
+ # Fallback for single-member workspaces (which omit `[manifest] members`):
142
+ # match packages whose `source` is a local variant — `virtual`, `editable`,
143
+ # or `directory` — per the `SourceWire` enum in the same file.
144
+ sig { params(packages: T::Array[T.untyped], manifest: T.untyped).returns(T::Set[String]) }
145
+ def root_package_names(packages, manifest)
146
+ declared = declared_workspace_members(manifest)
147
+ return declared unless declared.empty?
148
+
149
+ packages.each_with_object(Set.new) do |pkg, set|
150
+ next unless pkg.is_a?(Hash)
151
+
152
+ source = pkg["source"]
153
+ next unless source.is_a?(Hash)
154
+ next unless source.key?("virtual") || source.key?("editable") || source.key?("directory")
155
+
156
+ name = pkg["name"]
157
+ set << name if name.is_a?(String)
158
+ end
159
+ end
139
160
 
140
- uv_parser.run_in_parsed_context(UV_TREE_COMMAND).lines.each_with_object({}) do |line, rels|
141
- match = line.match(UV_TREE_LINE_REGEX)
142
- next unless match
161
+ sig { params(manifest: T.untyped).returns(T::Set[String]) }
162
+ def declared_workspace_members(manifest)
163
+ return Set.new unless manifest.is_a?(Hash)
143
164
 
144
- package = normalised_dependency_name(T.must(match[:package]))
145
- depth = T.must(match[:prefix]).scan(/(?:│ | )/).length
165
+ members = manifest["members"]
166
+ return Set.new unless members.is_a?(Array)
146
167
 
147
- relationship_stack[depth] = package
148
- relationship_stack.slice!(depth + 1, relationship_stack.length)
168
+ members.each_with_object(Set.new) do |name, set|
169
+ set << name if name.is_a?(String)
170
+ end
171
+ end
149
172
 
150
- parent = depth.zero? ? nil : relationship_stack[depth - 1]
151
- rels[package] ||= []
152
- next unless parent
173
+ # Mirrors uv's `ExportableRequirements::from_lock` (crates/uv-resolver/src/lock/export/mod.rs)
174
+ # when invoked with `--all-extras --all-groups`: each workspace root contributes its
175
+ # `dependencies` as direct runtime, `optional-dependencies` (all extras) as direct runtime,
176
+ # and `dev-dependencies` (all groups) as direct dev. We use --all-extras/--all-groups
177
+ # semantics because the dependency graph reports what *could* be installed, not what was
178
+ # selected for a particular sync.
179
+ sig do
180
+ params(packages: T::Array[T.untyped], root_names: T::Set[String])
181
+ .returns([T::Set[String], T::Set[String]])
182
+ end
183
+ def direct_dependency_names(packages, root_names)
184
+ runtime = T.let(Set.new, T::Set[String])
185
+ dev = T.let(Set.new, T::Set[String])
153
186
 
154
- rels[parent] ||= []
155
- rels[parent] << package
187
+ packages.each do |pkg|
188
+ next unless pkg.is_a?(Hash) && root_names.include?(pkg["name"])
189
+
190
+ collect_dep_names(pkg["dependencies"], runtime)
191
+ collect_dep_names_from_groups(pkg["optional-dependencies"], runtime)
192
+ collect_dep_names_from_groups(pkg["dev-dependencies"], dev)
156
193
  end
194
+
195
+ [runtime, dev]
157
196
  end
158
197
 
159
- sig { returns(Dependabot::Uv::FileParser) }
160
- def uv_parser
161
- T.cast(file_parser, Dependabot::Uv::FileParser)
198
+ sig { params(entries: T.untyped, collection: T.any(T::Set[String], T::Array[String])).void }
199
+ def collect_dep_names(entries, collection)
200
+ return unless entries.is_a?(Array)
201
+
202
+ entries.each do |entry|
203
+ name = lockfile_dependency_name(entry)
204
+ collection << name if name.is_a?(String)
205
+ end
206
+ end
207
+
208
+ sig { params(groups: T.untyped, collection: T.any(T::Set[String], T::Array[String])).void }
209
+ def collect_dep_names_from_groups(groups, collection)
210
+ return unless groups.is_a?(Hash)
211
+
212
+ groups.each_value { |entries| collect_dep_names(entries, collection) }
213
+ end
214
+
215
+ sig do
216
+ params(
217
+ pkg: T.untyped,
218
+ root_names: T::Set[String],
219
+ direct_runtime: T::Set[String],
220
+ direct_dev: T::Set[String]
221
+ ).returns(T.nilable(Dependabot::Dependency))
222
+ end
223
+ def build_dependency(pkg, root_names, direct_runtime, direct_dev)
224
+ return unless pkg.is_a?(Hash)
225
+
226
+ name = pkg["name"]
227
+ version = pkg["version"]
228
+ return unless name.is_a?(String) && version.is_a?(String)
229
+
230
+ # Root project packages get requirements: [] (indirect, runtime) to
231
+ # match the prior FileParser-derived behaviour where uv.lock packages
232
+ # without a pyproject entry surfaced as indirect.
233
+ groups = root_names.include?(name) ? [] : direct_groups_for(name, direct_runtime, direct_dev)
234
+ requirements = groups.empty? ? [] : [{ requirement: nil, file: "uv.lock", source: nil, groups: groups }]
235
+
236
+ Dependabot::Dependency.new(
237
+ name: normalised_dependency_name(name),
238
+ version: version,
239
+ requirements: requirements,
240
+ package_manager: "uv"
241
+ )
242
+ end
243
+
244
+ # A dependency listed under both runtime and dev groups stays runtime;
245
+ # uv's production check returns true if "dependencies" is present.
246
+ sig do
247
+ params(name: String, direct_runtime: T::Set[String], direct_dev: T::Set[String])
248
+ .returns(T::Array[String])
249
+ end
250
+ def direct_groups_for(name, direct_runtime, direct_dev)
251
+ return [RUNTIME_GROUP] if direct_runtime.include?(name)
252
+ return [DEV_GROUP] if direct_dev.include?(name)
253
+
254
+ []
162
255
  end
163
256
 
164
257
  sig { params(name: String).returns(String) }
@@ -171,23 +264,6 @@ module Dependabot
171
264
  "pypi"
172
265
  end
173
266
 
174
- # Strip extras (e.g. "[filecache]") from the dependency name for PURLs,
175
- # since the PURL should reference the base package only.
176
- sig { override.params(dependency: Dependabot::Dependency).returns(String) }
177
- def purl_name_for(dependency)
178
- NameNormaliser.normalise(dependency.name)
179
- end
180
-
181
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
182
- def pyproject_toml
183
- return @pyproject_toml if defined?(@pyproject_toml)
184
-
185
- @pyproject_toml = T.let(
186
- dependency_files.find { |f| f.name == "pyproject.toml" },
187
- T.nilable(Dependabot::DependencyFile)
188
- )
189
- end
190
-
191
267
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
192
268
  def uv_lock
193
269
  return @uv_lock if defined?(@uv_lock)
@@ -6,6 +6,7 @@ require "toml-rb"
6
6
  require "sorbet-runtime"
7
7
 
8
8
  require "dependabot/dependency"
9
+ require "dependabot/dependency_requirement"
9
10
  require "dependabot/errors"
10
11
  require "dependabot/uv/name_normaliser"
11
12
  require "dependabot/uv/requirement_parser"
@@ -33,14 +34,12 @@ module Dependabot
33
34
 
34
35
  sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
35
36
  def updated_requirements
36
- wrap_requirements(
37
- RequirementsUpdater.new(
38
- requirements: requirements,
39
- latest_resolvable_version: preferred_resolvable_version&.to_s,
40
- update_strategy: requirements_update_strategy,
41
- has_lockfile: requirements_text_file?
42
- ).updated_requirements
43
- )
37
+ RequirementsUpdater.new(
38
+ requirements: dependency.requirements,
39
+ latest_resolvable_version: preferred_resolvable_version&.to_s,
40
+ update_strategy: requirements_update_strategy,
41
+ has_lockfile: requirements_text_file?
42
+ ).updated_requirements
44
43
  end
45
44
 
46
45
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-uv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.382.0
4
+ version: 0.383.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.382.0
18
+ version: 0.383.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.382.0
25
+ version: 0.383.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dependabot-python
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.382.0
32
+ version: 0.383.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.382.0
39
+ version: 0.383.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -259,7 +259,6 @@ files:
259
259
  - helpers/lib/__init__.py
260
260
  - helpers/lib/hasher.py
261
261
  - helpers/lib/parser.py
262
- - helpers/requirements-3.9.txt
263
262
  - helpers/requirements.txt
264
263
  - helpers/run.py
265
264
  - lib/dependabot/uv.rb
@@ -302,7 +301,7 @@ licenses:
302
301
  - MIT
303
302
  metadata:
304
303
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
305
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.382.0
304
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.383.0
306
305
  rdoc_options: []
307
306
  require_paths:
308
307
  - lib
@@ -1,15 +0,0 @@
1
- # Python 3.9-compatible versions pinned to the last known working set before
2
- # packages dropped 3.9 support. Python 3.9 reached end-of-life on 2025-10-31.
3
- pip==24.2
4
- pip-tools==7.5.3
5
- flake8==7.3.0
6
- hashin==1.0.5
7
- pipenv==2024.4.1
8
- plette==2.1.0
9
- poetry==2.2.1
10
- # tomli is required for Python <3.11 (stdlib tomllib was added in 3.11).
11
- tomli==2.2.1
12
- uv==0.11.8
13
-
14
- # Some dependencies will only install if Cython is present
15
- Cython==3.2.4