dependabot-uv 0.352.0 → 0.354.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: 2182d20a6869566befc86859bd0260cbe23c3ce48b128c95c82ec5713af9f25b
4
- data.tar.gz: 0c7a49f6debe6d304cc07854636139d2504a9bd74baa1c72ff08a072b2ddea7c
3
+ metadata.gz: 9f6cd22ac8eea374c5385636da11e99168ed3d7a19425a73005bfe6dea57d462
4
+ data.tar.gz: f19a7c8dbc8208cfdba09e5f5513b04895b6ff4eb32c5cba0690bb21e2a2db6d
5
5
  SHA512:
6
- metadata.gz: ee6297977b3276e5d0661dfa1fc1400100a86dfacc0e916adef15ce504f9adb5707db9214c5ee017147202c8c2104333a9bf6c7f351fdd1ce8577e7b88d005ba
7
- data.tar.gz: b71b17c588efbd34281a174f229657173f924cdba4194ebb8073a37b3e2b7e37cdeec7effa71da66807291f0d455f76d3da8e8ab2ff560b4e35cbdab60f6781a
6
+ metadata.gz: f6d6de0188f952e54b6d3a5e50df326ffbdbaa8f53cc6c27034a489c1636e366a0b6bb15e5ac9210e71a15273dd1fb69430716fd93023b2d72a8eb34260ce252
7
+ data.tar.gz: a94975fe714eae7eadac990e4ce9c8d996ac4478b0016557245fb5fc2e5745591832f2098d1ca0383bcb7de025cf0e85b047aeefed2baaa8b602e90ac4d0431f
@@ -0,0 +1,210 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "toml-rb"
5
+ require "sorbet-runtime"
6
+ require "dependabot/dependency_file"
7
+
8
+ module Dependabot
9
+ module Uv
10
+ class FileFetcher < Dependabot::FileFetchers::Base
11
+ class WorkspaceFetcher
12
+ extend T::Sig
13
+
14
+ README_FILENAMES = T.let(%w(README.md README.rst README.txt README).freeze, T::Array[String])
15
+
16
+ sig do
17
+ params(
18
+ file_fetcher: Dependabot::Uv::FileFetcher,
19
+ pyproject: T.nilable(Dependabot::DependencyFile)
20
+ ).void
21
+ end
22
+ def initialize(file_fetcher, pyproject)
23
+ @file_fetcher = file_fetcher
24
+ @pyproject = pyproject
25
+ @parsed_pyproject = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
26
+ end
27
+
28
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
29
+ def workspace_member_files
30
+ return [] unless @pyproject
31
+
32
+ workspace_member_paths.flat_map do |member_path|
33
+ member_pyproject = fetch_workspace_member_pyproject(member_path)
34
+ member_readmes = fetch_readme_files_for(member_path, member_pyproject)
35
+
36
+ [member_pyproject] + member_readmes
37
+ rescue Dependabot::DependencyFileNotFound
38
+ []
39
+ end
40
+ end
41
+
42
+ sig { returns(T::Array[{ name: String, file: String }]) }
43
+ def uv_sources_workspace_dependencies
44
+ return [] unless @pyproject
45
+
46
+ uv_sources = parsed_pyproject.dig("tool", "uv", "sources")
47
+ return [] unless uv_sources
48
+
49
+ uv_sources.filter_map do |name, source_config|
50
+ if source_config.is_a?(Hash) && source_config["workspace"] == true
51
+ {
52
+ name: T.cast(name, String),
53
+ file: @pyproject.name
54
+ }
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ sig { params(member_path: String).returns(Dependabot::DependencyFile) }
62
+ def fetch_workspace_member_pyproject(member_path)
63
+ pyproject_path = clean_path(File.join(member_path, "pyproject.toml"))
64
+ pyproject_file = fetch_file_from_host(pyproject_path, fetch_submodules: true)
65
+ pyproject_file.support_file = true
66
+ pyproject_file
67
+ end
68
+
69
+ sig do
70
+ params(
71
+ path: String,
72
+ pyproject_file: Dependabot::DependencyFile
73
+ ).returns(T::Array[Dependabot::DependencyFile])
74
+ end
75
+ def fetch_readme_files_for(path, pyproject_file)
76
+ readme_candidates = readme_candidates_from_pyproject(pyproject_file)
77
+ is_root_project = path == directory
78
+
79
+ readme_candidates.filter_map do |filename|
80
+ file = fetch_readme_file(filename, path, is_root_project)
81
+ next unless file
82
+
83
+ file.support_file = true
84
+ file
85
+ rescue Dependabot::DependencyFileNotFound
86
+ nil
87
+ end
88
+ rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
89
+ []
90
+ end
91
+
92
+ sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
93
+ def readme_candidates_from_pyproject(pyproject_file)
94
+ parsed_content = TomlRB.parse(pyproject_file.content)
95
+ readme_declaration = parsed_content.dig("project", "readme")
96
+
97
+ case readme_declaration
98
+ when String then [readme_declaration]
99
+ when Hash
100
+ readme_declaration["file"].is_a?(String) ? [T.cast(readme_declaration["file"], String)] : README_FILENAMES
101
+ else
102
+ README_FILENAMES
103
+ end
104
+ end
105
+
106
+ sig do
107
+ params(
108
+ filename: String,
109
+ path: String,
110
+ is_root_project: T::Boolean
111
+ ).returns(T.nilable(Dependabot::DependencyFile))
112
+ end
113
+ def fetch_readme_file(filename, path, is_root_project)
114
+ if is_root_project
115
+ fetch_file_if_present(filename)
116
+ else
117
+ file_path = clean_path(File.join(path, filename))
118
+ fetch_file_from_host(file_path, fetch_submodules: true)
119
+ end
120
+ end
121
+
122
+ sig { returns(T::Array[String]) }
123
+ def workspace_member_paths
124
+ return [] unless @pyproject
125
+
126
+ members = parsed_pyproject.dig("tool", "uv", "workspace", "members")
127
+ return [] unless members.is_a?(Array)
128
+
129
+ members.grep(String).flat_map { |pattern| expand_workspace_pattern(pattern) }
130
+ end
131
+
132
+ sig { params(pattern: String).returns(T::Array[String]) }
133
+ def expand_workspace_pattern(pattern)
134
+ return [pattern] unless pattern.include?("*")
135
+
136
+ base_directory = extract_base_directory_from_glob(pattern)
137
+ directory_paths = fetch_directory_paths_for_matching(base_directory)
138
+ match_paths_against_pattern(directory_paths, pattern)
139
+ end
140
+
141
+ sig { params(glob_pattern: String).returns(String) }
142
+ def extract_base_directory_from_glob(glob_pattern)
143
+ pattern_without_dot_slash = glob_pattern.gsub(%r{^\./}, "")
144
+ path_before_glob = pattern_without_dot_slash.split("*").first&.gsub(%r{(?<=/)[^/]*$}, "") || "."
145
+ path_before_glob.empty? ? "." : path_before_glob.chomp("/")
146
+ end
147
+
148
+ sig { params(base_dir: String).returns(T::Array[String]) }
149
+ def fetch_directory_paths_for_matching(base_dir)
150
+ normalized_directory = directory.gsub(%r{(^/|/$)}, "")
151
+
152
+ repo_contents(dir: base_dir, raise_errors: false)
153
+ .select { |file| T.unsafe(file).type == "dir" }
154
+ .map { |f| T.unsafe(f).path.gsub(%r{^/?#{Regexp.escape(normalized_directory)}/?}, "") }
155
+ end
156
+
157
+ sig { params(paths: T::Array[String], pattern: String).returns(T::Array[String]) }
158
+ def match_paths_against_pattern(paths, pattern)
159
+ pattern_without_dot_slash = pattern.gsub(%r{^\./}, "")
160
+ paths.select { |path| File.fnmatch?(pattern_without_dot_slash, path, File::FNM_PATHNAME) }
161
+ end
162
+
163
+ sig { returns(T::Hash[String, T.untyped]) }
164
+ def parsed_pyproject
165
+ cached = @parsed_pyproject
166
+ return cached if cached
167
+ return {} unless @pyproject
168
+
169
+ @parsed_pyproject = TomlRB.parse(@pyproject.content)
170
+ end
171
+
172
+ # Delegate methods to file_fetcher
173
+ sig { params(path: T.nilable(T.any(Pathname, String))).returns(String) }
174
+ def clean_path(path)
175
+ @file_fetcher.send(:cleanpath, path)
176
+ end
177
+
178
+ sig do
179
+ params(
180
+ filename: String,
181
+ fetch_submodules: T::Boolean
182
+ ).returns(Dependabot::DependencyFile)
183
+ end
184
+ def fetch_file_from_host(filename, fetch_submodules: false)
185
+ @file_fetcher.send(:fetch_file_from_host, filename, fetch_submodules: fetch_submodules)
186
+ end
187
+
188
+ sig { params(filename: String).returns(T.nilable(Dependabot::DependencyFile)) }
189
+ def fetch_file_if_present(filename)
190
+ @file_fetcher.send(:fetch_file_if_present, filename)
191
+ end
192
+
193
+ sig do
194
+ params(
195
+ dir: T.nilable(String),
196
+ raise_errors: T::Boolean
197
+ ).returns(T::Array[OpenStruct])
198
+ end
199
+ def repo_contents(dir: nil, raise_errors: true)
200
+ @file_fetcher.send(:repo_contents, dir: dir, raise_errors: raise_errors)
201
+ end
202
+
203
+ sig { returns(String) }
204
+ def directory
205
+ @file_fetcher.send(:directory)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -12,6 +12,7 @@ require "dependabot/uv/requirements_file_matcher"
12
12
  require "dependabot/uv/requirement_parser"
13
13
  require "dependabot/uv/file_parser/pyproject_files_parser"
14
14
  require "dependabot/uv/file_parser/python_requirement_parser"
15
+ require "dependabot/uv/file_fetcher/workspace_fetcher"
15
16
  require "dependabot/errors"
16
17
  require "dependabot/file_filtering"
17
18
 
@@ -94,6 +95,7 @@ module Dependabot
94
95
 
95
96
  fetched_files += uv_lock_files
96
97
  fetched_files += project_files
98
+ fetched_files += workspace_member_files
97
99
  fetched_files << python_version_file if python_version_file
98
100
 
99
101
  uniques = uniq_files(fetched_files)
@@ -119,38 +121,20 @@ module Dependabot
119
121
  end
120
122
 
121
123
  sig { returns(T::Array[Dependabot::DependencyFile]) }
122
- def readme_files
123
- return [] unless pyproject
124
+ def workspace_member_files
125
+ workspace_fetcher.workspace_member_files
126
+ end
124
127
 
125
- # Attempt to read the readme declaration from the pyproject. Accept both simplified
126
- # string form and table form ( { file = "..." } ).
127
- readme_decl = nil
128
- begin
129
- readme_decl = parsed_pyproject.dig("project", "readme")
130
- rescue TomlRB::ParseError
131
- # If the pyproject is unparseable fail later in parsed_pyproject.
132
- end
128
+ sig { returns(WorkspaceFetcher) }
129
+ def workspace_fetcher
130
+ @workspace_fetcher ||= T.let(WorkspaceFetcher.new(self, pyproject), T.nilable(WorkspaceFetcher))
131
+ end
133
132
 
134
- candidate_names =
135
- case readme_decl
136
- when String then [readme_decl]
137
- when Hash
138
- if readme_decl["file"].is_a?(String)
139
- [T.cast(readme_decl["file"], String)]
140
- else
141
- README_FILENAMES
142
- end
143
- else
144
- README_FILENAMES
145
- end
133
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
134
+ def readme_files
135
+ return [] unless pyproject
146
136
 
147
- candidate_names.filter_map do |filename|
148
- file = fetch_file_if_present(filename)
149
- file.support_file = true if file
150
- file
151
- rescue Dependabot::DependencyFileNotFound
152
- nil
153
- end
137
+ workspace_fetcher.send(:fetch_readme_files_for, directory, T.must(pyproject))
154
138
  end
155
139
 
156
140
  sig { returns(T::Array[Dependabot::DependencyFile]) }
@@ -472,6 +456,11 @@ module Dependabot
472
456
  end
473
457
  end
474
458
 
459
+ sig { returns(T::Array[{ name: String, file: String }]) }
460
+ def uv_sources_workspace_dependencies
461
+ workspace_fetcher.uv_sources_workspace_dependencies
462
+ end
463
+
475
464
  sig { params(path: T.nilable(T.any(Pathname, String))).returns(T::Array[Dependabot::DependencyFile]) }
476
465
  def fetch_requirement_files_from_path(path = nil)
477
466
  contents = path ? repo_contents(dir: path) : repo_contents
@@ -37,6 +37,7 @@ module Dependabot
37
37
  "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1",
38
38
  String
39
39
  )
40
+ PYTHON_VERSION_REGEX = T.let(/--python-version[=\s]+(?<version>\d+\.\d+(?:\.\d+)?)/, Regexp)
40
41
 
41
42
  sig { returns(T::Array[Dependabot::Dependency]) }
42
43
  attr_reader :dependencies
@@ -475,6 +476,8 @@ module Dependabot
475
476
  /--index-url=\S+/, "--index-url=<index_url>"
476
477
  ).sub(
477
478
  /--extra-index-url=\S+/, "--extra-index-url=<extra_index_url>"
479
+ ).sub(
480
+ /--python-version=\S+/, "--python-version=<python_version>"
478
481
  )
479
482
  end
480
483
 
@@ -492,21 +495,27 @@ module Dependabot
492
495
 
493
496
  sig { params(requirements_file: Dependabot::DependencyFile).returns(T::Array[String]) }
494
497
  def uv_compile_options_from_compiled_file(requirements_file)
498
+ content = T.must(requirements_file.content)
495
499
  options = ["--output-file=#{requirements_file.name}"]
496
- options << "--emit-index-url" if T.must(requirements_file.content).include?("index-url http")
497
- options << "--generate-hashes" if T.must(requirements_file.content).include?("--hash=sha")
498
- options << "--no-annotate" unless T.must(requirements_file.content).include?("# via ")
499
- options << "--pre" if T.must(requirements_file.content).include?("--pre")
500
- options << "--no-strip-extras" if T.must(requirements_file.content).include?("--no-strip-extras")
501
-
502
- if T.must(requirements_file.content).include?("--no-binary") ||
503
- T.must(requirements_file.content).include?("--only-binary")
504
- options << "--emit-build-options"
505
- end
500
+ options << "--emit-index-url" if content.include?("index-url http")
501
+ options << "--generate-hashes" if content.include?("--hash=sha")
502
+ options << "--no-annotate" unless content.include?("# via ")
503
+ options << "--pre" if content.include?("--pre")
504
+ options << "--no-strip-extras" if content.include?("--no-strip-extras")
505
+ options << "--emit-build-options" if content.include?("--no-binary") || content.include?("--only-binary")
506
+ options << "--universal" if content.include?("--universal")
507
+
508
+ python_version_option = extract_python_version_option(content)
509
+ options << python_version_option if python_version_option
510
+
511
+ options.compact
512
+ end
506
513
 
507
- options << "--universal" if T.must(requirements_file.content).include?("--universal")
514
+ sig { params(content: String).returns(T.nilable(String)) }
515
+ def extract_python_version_option(content)
516
+ return unless (match = PYTHON_VERSION_REGEX.match(content))
508
517
 
509
- options
518
+ "--python-version=#{match[:version]}"
510
519
  end
511
520
 
512
521
  sig { returns(T::Array[String]) }
@@ -140,7 +140,7 @@ module Dependabot
140
140
  def replace_dep(dep, content, new_r, old_r)
141
141
  new_req = new_r[:requirement]
142
142
  old_req = old_r[:requirement]
143
- escaped_name = Regexp.escape(dep.name)
143
+ escaped_name = escape_package_name(dep.name)
144
144
 
145
145
  regex = /(["']#{escaped_name})([^"']+)(["'])/x
146
146
 
@@ -394,8 +394,9 @@ module Dependabot
394
394
  end
395
395
 
396
396
  sig { params(name: T.any(String, Symbol)).returns(String) }
397
- def escape(name)
398
- Regexp.escape(name).gsub("\\-", "[-_.]")
397
+ def escape_package_name(name)
398
+ # Per PEP 503, Python package names normalize -, _, and . to the same character
399
+ Regexp.escape(name).gsub(/\\[-_.]/, "[-_.]")
399
400
  end
400
401
 
401
402
  sig { params(file: T.nilable(DependencyFile)).returns(T::Boolean) }
@@ -476,6 +477,8 @@ module Dependabot
476
477
 
477
478
  sig { returns(T::Boolean) }
478
479
  def create_or_update_lock_file?
480
+ return true if lockfile && T.must(dependency).requirements.empty?
481
+
479
482
  T.must(dependency).requirements.select { _1[:file].end_with?(*REQUIRED_FILES) }.any?
480
483
  end
481
484
  end
@@ -39,6 +39,7 @@ module Dependabot
39
39
  RESOLUTION_IMPOSSIBLE_ERROR = T.let("ResolutionImpossible", String)
40
40
  ERROR_REGEX = T.let(/(?<=ERROR\:\W).*$/, Regexp)
41
41
  UV_UNRESOLVABLE_REGEX = T.let(/ × No solution found when resolving dependencies:[\s\S]*$/, Regexp)
42
+ PYTHON_VERSION_REGEX = T.let(/--python-version[=\s]+(?<version>\d+\.\d+(?:\.\d+)?)/, Regexp)
42
43
 
43
44
  sig { returns(Dependabot::Dependency) }
44
45
  attr_reader :dependency
@@ -272,6 +273,8 @@ module Dependabot
272
273
  /--index-url=\S+/, "--index-url=<index_url>"
273
274
  ).sub(
274
275
  /--extra-index-url=\S+/, "--extra-index-url=<extra_index_url>"
276
+ ).sub(
277
+ /--python-version=\S+/, "--python-version=<python_version>"
275
278
  )
276
279
  end
277
280
 
@@ -342,10 +345,19 @@ module Dependabot
342
345
 
343
346
  options << "--universal" if T.must(requirements_file.content).include?("--universal")
344
347
 
345
- options
348
+ options << extract_python_version_option(requirements_file)
349
+
350
+ options.compact
346
351
  end
347
352
  # rubocop:enable Metrics/AbcSize
348
353
 
354
+ sig { params(requirements_file: Dependabot::DependencyFile).returns(T.nilable(String)) }
355
+ def extract_python_version_option(requirements_file)
356
+ return unless (match = PYTHON_VERSION_REGEX.match(T.must(requirements_file.content)))
357
+
358
+ "--python-version=#{match[:version]}"
359
+ end
360
+
349
361
  sig { returns(T::Hash[String, String]) }
350
362
  def python_env
351
363
  env = {}
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.352.0
4
+ version: 0.354.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.352.0
18
+ version: 0.354.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.352.0
25
+ version: 0.354.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.352.0
32
+ version: 0.354.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.352.0
39
+ version: 0.354.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -264,6 +264,7 @@ files:
264
264
  - lib/dependabot/uv.rb
265
265
  - lib/dependabot/uv/authed_url_builder.rb
266
266
  - lib/dependabot/uv/file_fetcher.rb
267
+ - lib/dependabot/uv/file_fetcher/workspace_fetcher.rb
267
268
  - lib/dependabot/uv/file_parser.rb
268
269
  - lib/dependabot/uv/file_parser/pyproject_files_parser.rb
269
270
  - lib/dependabot/uv/file_parser/python_requirement_parser.rb
@@ -296,7 +297,7 @@ licenses:
296
297
  - MIT
297
298
  metadata:
298
299
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
299
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.352.0
300
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.354.0
300
301
  rdoc_options: []
301
302
  require_paths:
302
303
  - lib