dependabot-uv 0.363.0 → 0.364.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: c00b324d4b8b8b0070b03ba16767d40b0f8130871c78240c1099815277664f71
4
- data.tar.gz: f4bf0c298a4b4fbb1bab69c9af1bda513a6bdf5986f973ccfe8b233af51a301e
3
+ metadata.gz: 66f81d0ac637cdaa3193618598079972af284af23add69f280b5dc5eb26e988d
4
+ data.tar.gz: 0a10dc4db04f2e311feef435dc31c35dabf6f15ad8f02b6ba5d64daef384a615
5
5
  SHA512:
6
- metadata.gz: 45595b8e72d691dcb17bb747eff12e0c2ecd0ccd5afd9e6294eed9adbe2c82d05f8f160a9631d30254c67385084e283376b2473d5ad308992168acef273650eb
7
- data.tar.gz: 1b2da7bc589e4a0a1f094f67a4caeefea2512b9b34ab79f16765fd78645d6ed3389e8ef2a60bf90548a2d56ae48d08b4c29fcf66b12d3bcb5ce9bde450cbe7a8
6
+ metadata.gz: 186c1e97d18b0166ffd052abafbaffcfe18b880daf3c54dc6e065d5ae6f525c7c58d5813205339daa3821ca9e3aae619c311c402ce888009f6b7eece1d88fb90
7
+ data.tar.gz: e6c02a262d85ac3c593603cdf2e2ed519c3174470e1fe36827708297db5ec191da41c2cb96d63d22ecab5d46a8f03712d2ac2430bcab6a784b29a964ee40da82
@@ -0,0 +1,196 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/dependency_graphers"
7
+ require "dependabot/dependency_graphers/base"
8
+ require "dependabot/uv/file_parser"
9
+ require "toml-rb"
10
+
11
+ module Dependabot
12
+ module Uv
13
+ class DependencyGrapher < Dependabot::DependencyGraphers::Base
14
+ UV_LOCK_COMMAND = T.let("pyenv exec uv lock --color never --no-progress && cat uv.lock", String)
15
+ UV_TREE_COMMAND = T.let("pyenv exec uv tree -q --color never --no-progress --frozen", String)
16
+
17
+ # Used to capture package lines from `uv tree` output.
18
+ #
19
+ # Example output:
20
+ # ├── flask v3.1.3
21
+ # │ ├── click v8.3.1
22
+ # │ └── jinja2 v3.1.6
23
+ # │ └── markupsafe v3.0.3
24
+ #
25
+ # The `prefix` contains tree-depth segments (`│ ` or ` `) and
26
+ # `package` is the dependency name token before the `v<version>` marker.
27
+ UV_TREE_LINE_REGEX = T.let(
28
+ /^(?<prefix>(?:(?:│ )|(?: ))*)(?:├──|└──)\s(?<package>.+?)\sv[^\s]+(?:\s+\(.*\))?$/,
29
+ Regexp
30
+ )
31
+
32
+ sig { override.returns(Dependabot::DependencyFile) }
33
+ def relevant_dependency_file
34
+ # This cannot realistically happen as the parser will throw a runtime error
35
+ # on init without a pyproject.toml file,
36
+ # but this will avoid surprises if anything changes.
37
+ raise DependabotError, "No pyproject.toml present in dependency files." unless pyproject_toml
38
+
39
+ T.must(pyproject_toml)
40
+ end
41
+
42
+ private
43
+
44
+ sig { override.params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
45
+ def fetch_subdependencies(dependency)
46
+ dependency_names = @dependencies.map(&:name)
47
+ package_relationships.fetch(dependency.name, []).select { |child| dependency_names.include?(child) }
48
+ end
49
+
50
+ sig { returns(T::Hash[String, T::Array[String]]) }
51
+ def package_relationships
52
+ @package_relationships ||= T.let(
53
+ fetch_package_relationships,
54
+ T.nilable(T::Hash[String, T::Array[String]])
55
+ )
56
+ end
57
+
58
+ # See UV tree docs https://docs.astral.sh/uv/reference/cli/#uv-tree
59
+ # First try extracting relationships from uv.lock directly. If there is no
60
+ # lockfile, generate one in a temporary parsed context and parse that.
61
+ # If lockfile parsing fails for any reason, fall back to uv tree output.
62
+ sig { returns(T::Hash[String, T::Array[String]]) }
63
+ def fetch_package_relationships
64
+ return package_relationships_from_lockfile(T.must(T.must(uv_lock).content)) if uv_lock
65
+
66
+ begin
67
+ Dependabot.logger.info("No uv.lock present, generating ephemeral lockfile for dependency graphing")
68
+ generated_lockfile = uv_parser.run_in_parsed_context(UV_LOCK_COMMAND)
69
+ return package_relationships_from_lockfile(generated_lockfile)
70
+ rescue StandardError => e
71
+ Dependabot.logger.warn("Failed to build dependency graph from uv.lock: #{e.message}")
72
+ Dependabot.logger.info("Falling back to parsing uv tree output")
73
+ end
74
+
75
+ package_relationships_from_tree
76
+ end
77
+
78
+ sig { params(lockfile_content: String).returns(T::Hash[String, T::Array[String]]) }
79
+ def package_relationships_from_lockfile(lockfile_content)
80
+ lockfile_packages(lockfile_content).each_with_object({}) do |package_data, rels|
81
+ parent = lockfile_parent_name(package_data)
82
+ next unless parent
83
+
84
+ rels[parent] ||= []
85
+ rels[parent].concat(lockfile_child_names(package_data))
86
+ end
87
+ rescue StandardError => e
88
+ Dependabot.logger.warn("Failed to parse uv.lock relationships: #{e.message}")
89
+ {}
90
+ end
91
+
92
+ sig { params(lockfile_content: String).returns(T::Array[T.untyped]) }
93
+ def lockfile_packages(lockfile_content)
94
+ parsed = TomlRB.parse(lockfile_content)
95
+ T.cast(parsed.fetch("package", []), T::Array[T.untyped])
96
+ end
97
+
98
+ sig { params(package_data: T.untyped).returns(T.nilable(String)) }
99
+ def lockfile_parent_name(package_data)
100
+ return unless package_data.is_a?(Hash)
101
+
102
+ package_name = package_data["name"]
103
+ return unless package_name.is_a?(String)
104
+
105
+ normalised_dependency_name(package_name)
106
+ end
107
+
108
+ sig { params(package_data: T.untyped).returns(T::Array[String]) }
109
+ def lockfile_child_names(package_data)
110
+ dependencies =
111
+ if package_data.is_a?(Hash)
112
+ T.cast(package_data["dependencies"], T.nilable(T::Array[T.untyped])) || []
113
+ else
114
+ []
115
+ end
116
+
117
+ dependencies.filter_map do |dependency|
118
+ dependency_name = lockfile_dependency_name(dependency)
119
+ normalised_dependency_name(dependency_name) if dependency_name
120
+ end
121
+ end
122
+
123
+ sig { params(dependency_data: T.untyped).returns(T.nilable(String)) }
124
+ def lockfile_dependency_name(dependency_data)
125
+ if dependency_data.is_a?(Hash)
126
+ name = dependency_data["name"]
127
+ return name if name.is_a?(String)
128
+ end
129
+
130
+ return dependency_data if dependency_data.is_a?(String)
131
+
132
+ nil
133
+ end
134
+
135
+ sig { returns(T::Hash[String, T::Array[String]]) }
136
+ def package_relationships_from_tree
137
+ relationship_stack = T.let([], T::Array[String])
138
+
139
+ uv_parser.run_in_parsed_context(UV_TREE_COMMAND).lines.each_with_object({}) do |line, rels|
140
+ match = line.match(UV_TREE_LINE_REGEX)
141
+ next unless match
142
+
143
+ package = normalised_dependency_name(T.must(match[:package]))
144
+ depth = T.must(match[:prefix]).scan(/(?:│ | )/).length
145
+
146
+ relationship_stack[depth] = package
147
+ relationship_stack.slice!(depth + 1, relationship_stack.length)
148
+
149
+ parent = depth.zero? ? nil : relationship_stack[depth - 1]
150
+ rels[package] ||= []
151
+ next unless parent
152
+
153
+ rels[parent] ||= []
154
+ rels[parent] << package
155
+ end
156
+ end
157
+
158
+ sig { returns(Dependabot::Uv::FileParser) }
159
+ def uv_parser
160
+ T.cast(file_parser, Dependabot::Uv::FileParser)
161
+ end
162
+
163
+ sig { params(name: String).returns(String) }
164
+ def normalised_dependency_name(name)
165
+ Dependabot::Uv::FileParser.normalize_dependency_name(name)
166
+ end
167
+
168
+ sig { override.params(_dependency: Dependabot::Dependency).returns(String) }
169
+ def purl_pkg_for(_dependency)
170
+ "pypi"
171
+ end
172
+
173
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
174
+ def pyproject_toml
175
+ return @pyproject_toml if defined?(@pyproject_toml)
176
+
177
+ @pyproject_toml = T.let(
178
+ dependency_files.find { |f| f.name == "pyproject.toml" },
179
+ T.nilable(Dependabot::DependencyFile)
180
+ )
181
+ end
182
+
183
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
184
+ def uv_lock
185
+ return @uv_lock if defined?(@uv_lock)
186
+
187
+ @uv_lock = T.let(
188
+ dependency_files.find { |f| f.name == "uv.lock" },
189
+ T.nilable(Dependabot::DependencyFile)
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ Dependabot::DependencyGraphers.register("uv", Dependabot::Uv::DependencyGrapher)
@@ -71,6 +71,21 @@ module Dependabot
71
71
  )
72
72
  end
73
73
 
74
+ sig { override.params(command: String).returns(String) }
75
+ def run_in_parsed_context(command)
76
+ SharedHelpers.in_a_temporary_directory do
77
+ dependency_files.each do |file|
78
+ path = file.name
79
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
80
+ File.write(path, file.content)
81
+ end
82
+
83
+ setup_python_environment
84
+
85
+ SharedHelpers.run_shell_command(command)
86
+ end
87
+ end
88
+
74
89
  # Normalize dependency names to match the PyPI index normalization
75
90
  sig { params(name: String, extras: T::Array[String]).returns(String) }
76
91
  def self.normalize_dependency_name(name, extras = [])
@@ -94,8 +109,7 @@ module Dependabot
94
109
  def python_requirement_parser
95
110
  @python_requirement_parser ||= T.let(
96
111
  PythonRequirementParser.new(
97
- dependency_files:
98
- dependency_files
112
+ dependency_files: dependency_files
99
113
  ),
100
114
  T.nilable(PythonRequirementParser)
101
115
  )
@@ -359,13 +359,13 @@ module Dependabot
359
359
  .select { |cred| cred["type"] == "python_index" }
360
360
  .reject { |cred| explicit_index?(cred) }
361
361
  .map do |cred|
362
- authed_url = AuthedUrlBuilder.authed_url(credential: cred)
362
+ authed_url = AuthedUrlBuilder.authed_url(credential: cred)
363
363
 
364
- if cred.replaces_base?
365
- "--default-index #{authed_url}"
366
- else
367
- "--index #{authed_url}"
368
- end
364
+ if cred.replaces_base?
365
+ "--default-index #{authed_url}"
366
+ else
367
+ "--index #{authed_url}"
368
+ end
369
369
  end
370
370
  end
371
371
 
@@ -421,18 +421,18 @@ module Dependabot
421
421
  .select { |cred| cred["type"] == "python_index" }
422
422
  .select { |cred| explicit_index?(cred) }
423
423
  .each do |cred|
424
- index_name = find_index_name_for_credential(cred)
425
- next unless index_name
424
+ index_name = find_index_name_for_credential(cred)
425
+ next unless index_name
426
426
 
427
- env_name = index_name.upcase.gsub(/[^A-Z0-9]/, "_")
427
+ env_name = index_name.upcase.gsub(/[^A-Z0-9]/, "_")
428
428
 
429
- env_vars["UV_INDEX_#{env_name}_USERNAME"] = cred["username"] if cred["username"]
429
+ env_vars["UV_INDEX_#{env_name}_USERNAME"] = cred["username"] if cred["username"]
430
430
 
431
- if cred["password"]
432
- env_vars["UV_INDEX_#{env_name}_PASSWORD"] = cred["password"]
433
- elsif cred["token"]
434
- env_vars["UV_INDEX_#{env_name}_PASSWORD"] = cred["token"]
435
- end
431
+ if cred["password"]
432
+ env_vars["UV_INDEX_#{env_name}_PASSWORD"] = cred["password"]
433
+ elsif cred["token"]
434
+ env_vars["UV_INDEX_#{env_name}_PASSWORD"] = cred["token"]
435
+ end
436
436
  end
437
437
 
438
438
  env_vars
data/lib/dependabot/uv.rb CHANGED
@@ -16,6 +16,7 @@ end
16
16
 
17
17
  # These all need to be required so the various classes can be registered in a
18
18
  # lookup table of package manager names to concrete classes.
19
+ require "dependabot/uv/dependency_grapher"
19
20
  require "dependabot/uv/file_fetcher"
20
21
  require "dependabot/uv/file_parser"
21
22
  require "dependabot/uv/update_checker"
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.363.0
4
+ version: 0.364.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.363.0
18
+ version: 0.364.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.363.0
25
+ version: 0.364.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.363.0
32
+ version: 0.364.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.363.0
39
+ version: 0.364.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -263,6 +263,7 @@ files:
263
263
  - helpers/run.py
264
264
  - lib/dependabot/uv.rb
265
265
  - lib/dependabot/uv/authed_url_builder.rb
266
+ - lib/dependabot/uv/dependency_grapher.rb
266
267
  - lib/dependabot/uv/file_fetcher.rb
267
268
  - lib/dependabot/uv/file_fetcher/workspace_fetcher.rb
268
269
  - lib/dependabot/uv/file_parser.rb
@@ -299,7 +300,7 @@ licenses:
299
300
  - MIT
300
301
  metadata:
301
302
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
302
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.363.0
303
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.364.0
303
304
  rdoc_options: []
304
305
  require_paths:
305
306
  - lib