dependabot-python 0.366.0 → 0.368.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 +4 -4
- data/lib/dependabot/python/dependency_grapher/lockfile_generator.rb +131 -0
- data/lib/dependabot/python/dependency_grapher.rb +77 -1
- data/lib/dependabot/python/file_parser/pyproject_files_parser.rb +13 -6
- data/lib/dependabot/python/file_parser/setup_file_parser.rb +15 -2
- data/lib/dependabot/python/file_parser.rb +10 -2
- data/lib/dependabot/python/file_updater/poetry_file_updater.rb +11 -8
- data/lib/dependabot/python/file_updater/requirement_file_updater.rb +36 -13
- data/lib/dependabot/python/file_updater/requirement_replacer.rb +29 -21
- data/lib/dependabot/python/file_updater/setup_file_sanitizer.rb +12 -3
- data/lib/dependabot/python/metadata_finder.rb +152 -20
- data/lib/dependabot/python/shared_file_fetcher.rb +10 -5
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b03d84191a73f9cd803b3db1fd32bc70638a4f607c2e9424a3900f143119f5f
|
|
4
|
+
data.tar.gz: 6d04a235c38eb76a1a86eb2f60e2f34c80c82ff5f342d997ff3c8a25011e42f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 793a849e50f2162b65f1da65521dfb7450081b84507fbb4134145ab5c72b63a916a59c68602bf41dea530d373cb0d2dd5e9a38fee9b6a5347b17848a8891054f
|
|
7
|
+
data.tar.gz: db96c2fd7be7c4b6ac473f29dc429f3c4163fb2dd38d4b1d48c19ea62727022ff6822166dd10e9c4a680eae24bc5f16f447bf9f8ec55b7909b8ae663698b29c6
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/dependency_file"
|
|
7
|
+
require "dependabot/shared_helpers"
|
|
8
|
+
require "dependabot/python/file_parser/python_requirement_parser"
|
|
9
|
+
require "dependabot/python/language_version_manager"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Python
|
|
13
|
+
class DependencyGrapher < Dependabot::DependencyGraphers::Base
|
|
14
|
+
class LockfileGenerator
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
LOCKFILE_NAME = "poetry.lock"
|
|
18
|
+
|
|
19
|
+
sig do
|
|
20
|
+
params(
|
|
21
|
+
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
22
|
+
credentials: T::Array[Dependabot::Credential]
|
|
23
|
+
).void
|
|
24
|
+
end
|
|
25
|
+
def initialize(dependency_files:, credentials:)
|
|
26
|
+
@dependency_files = dependency_files
|
|
27
|
+
@credentials = credentials
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
31
|
+
def generate
|
|
32
|
+
SharedHelpers.in_a_temporary_directory do
|
|
33
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
34
|
+
write_temporary_files
|
|
35
|
+
language_version_manager.install_required_python
|
|
36
|
+
run_poetry_lock
|
|
37
|
+
read_generated_lockfile
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
41
|
+
handle_generation_error(e)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
48
|
+
attr_reader :dependency_files
|
|
49
|
+
|
|
50
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
51
|
+
attr_reader :credentials
|
|
52
|
+
|
|
53
|
+
sig { void }
|
|
54
|
+
def write_temporary_files
|
|
55
|
+
dependency_files.each do |file|
|
|
56
|
+
path = file.name
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
58
|
+
File.write(path, file.content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
File.write(".python-version", language_version_manager.python_major_minor)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { void }
|
|
65
|
+
def run_poetry_lock
|
|
66
|
+
Dependabot.logger.info("Generating poetry.lock for dependency graphing")
|
|
67
|
+
|
|
68
|
+
# Use system git instead of the pure Python dulwich
|
|
69
|
+
run_poetry_command("pyenv exec poetry config system-git-client true")
|
|
70
|
+
|
|
71
|
+
# --no-interaction avoids password prompts
|
|
72
|
+
run_poetry_command("pyenv exec poetry lock --no-interaction")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
76
|
+
def read_generated_lockfile
|
|
77
|
+
unless File.exist?(LOCKFILE_NAME)
|
|
78
|
+
Dependabot.logger.warn("#{LOCKFILE_NAME} was not generated")
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
content = File.read(LOCKFILE_NAME)
|
|
83
|
+
|
|
84
|
+
Dependabot::DependencyFile.new(
|
|
85
|
+
name: LOCKFILE_NAME,
|
|
86
|
+
content: content,
|
|
87
|
+
directory: pyproject_directory
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { returns(String) }
|
|
92
|
+
def pyproject_directory
|
|
93
|
+
pyproject = dependency_files.find { |f| f.name == "pyproject.toml" }
|
|
94
|
+
pyproject&.directory || "/"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig { params(command: String).returns(String) }
|
|
98
|
+
def run_poetry_command(command)
|
|
99
|
+
SharedHelpers.run_shell_command(command, fingerprint: command)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { returns(LanguageVersionManager) }
|
|
103
|
+
def language_version_manager
|
|
104
|
+
@language_version_manager ||= T.let(
|
|
105
|
+
LanguageVersionManager.new(
|
|
106
|
+
python_requirement_parser: python_requirement_parser
|
|
107
|
+
),
|
|
108
|
+
T.nilable(LanguageVersionManager)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { returns(FileParser::PythonRequirementParser) }
|
|
113
|
+
def python_requirement_parser
|
|
114
|
+
@python_requirement_parser ||= T.let(
|
|
115
|
+
FileParser::PythonRequirementParser.new(
|
|
116
|
+
dependency_files: dependency_files
|
|
117
|
+
),
|
|
118
|
+
T.nilable(FileParser::PythonRequirementParser)
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
|
|
123
|
+
def handle_generation_error(error)
|
|
124
|
+
Dependabot.logger.error(
|
|
125
|
+
"Failed to generate #{LOCKFILE_NAME}: #{error.message}"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -16,12 +16,14 @@ require "toml-rb"
|
|
|
16
16
|
module Dependabot
|
|
17
17
|
module Python
|
|
18
18
|
class DependencyGrapher < Dependabot::DependencyGraphers::Base
|
|
19
|
+
require_relative "dependency_grapher/lockfile_generator"
|
|
20
|
+
|
|
19
21
|
sig { override.returns(Dependabot::DependencyFile) }
|
|
20
22
|
def relevant_dependency_file
|
|
21
23
|
dependency_files_by_package_manager = T.let(
|
|
22
24
|
{
|
|
23
25
|
PipenvPackageManager::NAME => [pipfile_lock, pipfile],
|
|
24
|
-
PoetryPackageManager::NAME => [
|
|
26
|
+
PoetryPackageManager::NAME => [committed_poetry_lock, pyproject_toml],
|
|
25
27
|
PipCompilePackageManager::NAME => [pip_compile_lockfile, pip_compile_manifest, pyproject_toml],
|
|
26
28
|
PipPackageManager::NAME => [pip_requirements_file, pyproject_toml, pipfile_lock, pipfile, setup_file,
|
|
27
29
|
setup_cfg_file]
|
|
@@ -36,13 +38,87 @@ module Dependabot
|
|
|
36
38
|
raise DependabotError, "No supported dependency file present."
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
sig { override.void }
|
|
42
|
+
def prepare!
|
|
43
|
+
if poetry_project_without_lockfile?
|
|
44
|
+
Dependabot.logger.info("No poetry.lock found, generating ephemeral lockfile for dependency graphing")
|
|
45
|
+
generate_ephemeral_lockfile!
|
|
46
|
+
emit_missing_lockfile_warning! if @ephemeral_lockfile_generated
|
|
47
|
+
end
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
39
51
|
private
|
|
40
52
|
|
|
53
|
+
# Returns the poetry.lock only if it was committed to the repo,
|
|
54
|
+
# not if it was generated ephemerally. This ensures that
|
|
55
|
+
# relevant_dependency_file reports the real manifest (pyproject.toml).
|
|
56
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
57
|
+
def committed_poetry_lock
|
|
58
|
+
return nil if @ephemeral_lockfile_generated
|
|
59
|
+
|
|
60
|
+
poetry_lock
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The file parser only identifies Poetry when poetry.lock is present,
|
|
64
|
+
# so we detect it independently by checking for [tool.poetry] in pyproject.toml.
|
|
65
|
+
# Within the python image, no other package manager uses this section
|
|
66
|
+
# (uv runs in a separate image).
|
|
67
|
+
sig { returns(T::Boolean) }
|
|
68
|
+
def poetry_project_without_lockfile?
|
|
69
|
+
return false if poetry_lock
|
|
70
|
+
return false unless pyproject_toml
|
|
71
|
+
|
|
72
|
+
parsed = TomlRB.parse(T.must(pyproject_toml&.content))
|
|
73
|
+
!parsed.dig("tool", "poetry").nil?
|
|
74
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
41
78
|
sig { returns(String) }
|
|
42
79
|
def python_package_manager
|
|
43
80
|
T.must(file_parser.ecosystem).package_manager.name
|
|
44
81
|
end
|
|
45
82
|
|
|
83
|
+
sig { void }
|
|
84
|
+
def generate_ephemeral_lockfile!
|
|
85
|
+
generator = LockfileGenerator.new(
|
|
86
|
+
dependency_files: dependency_files,
|
|
87
|
+
credentials: file_parser.credentials
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
ephemeral_lockfile = generator.generate
|
|
91
|
+
return unless ephemeral_lockfile
|
|
92
|
+
|
|
93
|
+
inject_ephemeral_lockfile(ephemeral_lockfile)
|
|
94
|
+
@ephemeral_lockfile_generated = T.let(true, T.nilable(T::Boolean))
|
|
95
|
+
|
|
96
|
+
Dependabot.logger.info(
|
|
97
|
+
"Successfully generated ephemeral #{ephemeral_lockfile.name} for dependency graphing"
|
|
98
|
+
)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Dependabot.logger.warn(
|
|
101
|
+
"Failed to generate ephemeral lockfile: #{e.message}. " \
|
|
102
|
+
"Dependency versions may not be resolved."
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(ephemeral_lockfile: Dependabot::DependencyFile).void }
|
|
107
|
+
def inject_ephemeral_lockfile(ephemeral_lockfile)
|
|
108
|
+
dependency_files << ephemeral_lockfile
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig { void }
|
|
112
|
+
def emit_missing_lockfile_warning!
|
|
113
|
+
Dependabot.logger.warn(
|
|
114
|
+
"No poetry.lock was found in this repository. " \
|
|
115
|
+
"Dependabot generated a temporary lockfile to determine exact dependency versions.\n\n" \
|
|
116
|
+
"To ensure consistent builds and security scanning, we recommend committing your poetry.lock file. " \
|
|
117
|
+
"Without a committed lockfile, resolved dependency versions may change between scans " \
|
|
118
|
+
"due to new package releases."
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
46
122
|
sig { override.params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
|
|
47
123
|
def fetch_subdependencies(dependency)
|
|
48
124
|
package_relationships.fetch(dependency.name, []).select { |child| dependency_name_set.include?(child) }
|
|
@@ -46,11 +46,7 @@ module Dependabot
|
|
|
46
46
|
def pyproject_dependencies
|
|
47
47
|
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
|
48
48
|
|
|
49
|
-
# Parse Poetry dependencies if [tool.poetry] section exists
|
|
50
49
|
dependencies += poetry_dependencies if using_poetry?
|
|
51
|
-
|
|
52
|
-
# Parse PEP 621/735 dependencies if those sections exist
|
|
53
|
-
# This handles hybrid projects that have both Poetry and PEP 621 sections
|
|
54
50
|
dependencies += pep621_pep735_dependencies if using_pep621? || using_pep735?
|
|
55
51
|
|
|
56
52
|
dependencies
|
|
@@ -101,7 +97,7 @@ module Dependabot
|
|
|
101
97
|
|
|
102
98
|
dependencies <<
|
|
103
99
|
Dependency.new(
|
|
104
|
-
name:
|
|
100
|
+
name: normalise(dep["name"]),
|
|
105
101
|
version: dep["version"]&.include?("*") ? nil : dep["version"],
|
|
106
102
|
requirements: [{
|
|
107
103
|
requirement: dep["requirement"],
|
|
@@ -109,7 +105,8 @@ module Dependabot
|
|
|
109
105
|
source: nil,
|
|
110
106
|
groups: [dep["requirement_type"]].compact
|
|
111
107
|
}],
|
|
112
|
-
package_manager: "pip"
|
|
108
|
+
package_manager: "pip",
|
|
109
|
+
metadata: extras_metadata(dep["extras"])
|
|
113
110
|
)
|
|
114
111
|
end
|
|
115
112
|
|
|
@@ -147,6 +144,16 @@ module Dependabot
|
|
|
147
144
|
NameNormaliser.normalise_including_extras(name, extras)
|
|
148
145
|
end
|
|
149
146
|
|
|
147
|
+
# Build metadata hash storing extras as a comma-separated string.
|
|
148
|
+
# Stored in metadata so the file updater can reconstruct the full
|
|
149
|
+
# PEP 621 declaration (e.g. "cachecontrol[filecache]>=0.14.0").
|
|
150
|
+
sig { params(extras: T::Array[String]).returns(T::Hash[Symbol, String]) }
|
|
151
|
+
def extras_metadata(extras)
|
|
152
|
+
return {} if extras.empty?
|
|
153
|
+
|
|
154
|
+
{ extras: extras.join(",") }
|
|
155
|
+
end
|
|
156
|
+
|
|
150
157
|
# @param req can be an Array, Hash or String that represents the constraints for a dependency
|
|
151
158
|
sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
|
|
152
159
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
@@ -43,7 +43,7 @@ module Dependabot
|
|
|
43
43
|
|
|
44
44
|
dependencies <<
|
|
45
45
|
Dependency.new(
|
|
46
|
-
name:
|
|
46
|
+
name: normalise(dep["name"]),
|
|
47
47
|
version: dep["version"]&.include?("*") ? nil : dep["version"],
|
|
48
48
|
requirements: [{
|
|
49
49
|
requirement: dep["requirement"],
|
|
@@ -51,7 +51,8 @@ module Dependabot
|
|
|
51
51
|
source: nil,
|
|
52
52
|
groups: [dep["requirement_type"]]
|
|
53
53
|
}],
|
|
54
|
-
package_manager: "pip"
|
|
54
|
+
package_manager: "pip",
|
|
55
|
+
metadata: extras_metadata(dep["extras"])
|
|
55
56
|
)
|
|
56
57
|
end
|
|
57
58
|
dependencies
|
|
@@ -184,6 +185,18 @@ module Dependabot
|
|
|
184
185
|
NameNormaliser.normalise_including_extras(name, extras)
|
|
185
186
|
end
|
|
186
187
|
|
|
188
|
+
sig { params(name: String).returns(String) }
|
|
189
|
+
def normalise(name)
|
|
190
|
+
NameNormaliser.normalise(name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sig { params(extras: T::Array[String]).returns(T::Hash[Symbol, String]) }
|
|
194
|
+
def extras_metadata(extras)
|
|
195
|
+
return {} if extras.empty?
|
|
196
|
+
|
|
197
|
+
{ extras: extras.join(",") }
|
|
198
|
+
end
|
|
199
|
+
|
|
187
200
|
sig { returns(T.untyped) }
|
|
188
201
|
def setup_file
|
|
189
202
|
dependency_files.find { |f| f.name == "setup.py" }
|
|
@@ -293,10 +293,11 @@ module Dependabot
|
|
|
293
293
|
|
|
294
294
|
dependencies <<
|
|
295
295
|
Dependency.new(
|
|
296
|
-
name:
|
|
296
|
+
name: NameNormaliser.normalise(name),
|
|
297
297
|
version: version&.include?("*") ? nil : version,
|
|
298
298
|
requirements: requirements,
|
|
299
|
-
package_manager: "pip"
|
|
299
|
+
package_manager: "pip",
|
|
300
|
+
metadata: extras_metadata(dep["extras"])
|
|
300
301
|
)
|
|
301
302
|
end
|
|
302
303
|
dependencies
|
|
@@ -467,6 +468,13 @@ module Dependabot
|
|
|
467
468
|
NameNormaliser.normalise_including_extras(name, extras)
|
|
468
469
|
end
|
|
469
470
|
|
|
471
|
+
sig { params(extras: T::Array[String]).returns(T::Hash[Symbol, String]) }
|
|
472
|
+
def extras_metadata(extras)
|
|
473
|
+
return {} if extras.empty?
|
|
474
|
+
|
|
475
|
+
{ extras: extras.join(",") }
|
|
476
|
+
end
|
|
477
|
+
|
|
470
478
|
sig { override.returns(T.untyped) }
|
|
471
479
|
def check_required_files
|
|
472
480
|
filenames = dependency_files.map(&:name)
|
|
@@ -49,7 +49,6 @@ module Dependabot
|
|
|
49
49
|
@language_version_manager = T.let(nil, T.nilable(LanguageVersionManager))
|
|
50
50
|
@python_requirement_parser = T.let(nil, T.nilable(FileParser::PythonRequirementParser))
|
|
51
51
|
@updated_pyproject_content = T.let(nil, T.nilable(String))
|
|
52
|
-
@python_helper_path = T.let(nil, T.nilable(String))
|
|
53
52
|
@poetry_lock = T.let(nil, T.nilable(Dependabot::DependencyFile))
|
|
54
53
|
end
|
|
55
54
|
|
|
@@ -363,7 +362,7 @@ module Dependabot
|
|
|
363
362
|
write_temporary_dependency_files(pyproject_content)
|
|
364
363
|
|
|
365
364
|
SharedHelpers.run_helper_subprocess(
|
|
366
|
-
command: "pyenv exec python3 #{python_helper_path}",
|
|
365
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
|
367
366
|
function: "get_pyproject_hash",
|
|
368
367
|
args: [T.cast(dir, Pathname).to_s]
|
|
369
368
|
)
|
|
@@ -386,7 +385,16 @@ module Dependabot
|
|
|
386
385
|
|
|
387
386
|
sig { params(dep: Dependabot::Dependency, old_req: String).returns(Regexp) }
|
|
388
387
|
def pep621_declaration_regex(dep, old_req)
|
|
389
|
-
/(?<declaration>["']#{escape(dep)}#{Regexp.escape(old_req)}["'])/mi
|
|
388
|
+
/(?<declaration>["']#{escape(dep)}#{extras_pattern(dep)}#{Regexp.escape(old_req)}["'])/mi
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Reconstructs extras from metadata for PEP 621 regex matching.
|
|
392
|
+
sig { params(dep: Dependabot::Dependency).returns(String) }
|
|
393
|
+
def extras_pattern(dep)
|
|
394
|
+
extras_str = dep.metadata[:extras]
|
|
395
|
+
return "" unless extras_str.is_a?(String) && !extras_str.empty?
|
|
396
|
+
|
|
397
|
+
"\\[" + extras_str.split(",").map { |e| Regexp.escape(e.strip) }.join(",\\s*") + "\\]"
|
|
390
398
|
end
|
|
391
399
|
|
|
392
400
|
sig { params(dep: Dependency).returns(String) }
|
|
@@ -446,11 +454,6 @@ module Dependabot
|
|
|
446
454
|
@lockfile ||= poetry_lock
|
|
447
455
|
end
|
|
448
456
|
|
|
449
|
-
sig { returns(String) }
|
|
450
|
-
def python_helper_path
|
|
451
|
-
NativeHelpers.python_helper_path
|
|
452
|
-
end
|
|
453
|
-
|
|
454
457
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
455
458
|
def poetry_lock
|
|
456
459
|
dependency_files.find { |f| f.name == "poetry.lock" }
|
|
@@ -55,33 +55,56 @@ module Dependabot
|
|
|
55
55
|
|
|
56
56
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
57
57
|
def fetch_updated_dependency_files
|
|
58
|
-
|
|
58
|
+
updated_contents = T.let({}, T::Hash[String, String])
|
|
59
|
+
|
|
60
|
+
unique_requirement_changes.each do |pair|
|
|
61
|
+
new_req = T.must(pair[0])
|
|
62
|
+
old_req = pair[1]
|
|
63
|
+
filename = new_req.fetch(:file)
|
|
64
|
+
content = updated_contents[filename] || T.must(T.must(get_original_file(filename)).content)
|
|
65
|
+
updated_contents[filename] = updated_requirement_or_setup_file_content(content, new_req, old_req)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
updated_contents.filter_map do |filename, content|
|
|
69
|
+
file = T.must(get_original_file(filename)).dup
|
|
70
|
+
next if content == T.must(file.content)
|
|
71
|
+
|
|
72
|
+
file.content = content
|
|
73
|
+
file
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Deduplicates requirements that share the same file and requirement strings.
|
|
78
|
+
# The replacer's regex matches all extras variants at once, so one call per
|
|
79
|
+
# unique (file, old_requirement, new_requirement) is sufficient.
|
|
80
|
+
sig { returns(T::Array[T::Array[T.nilable(T::Hash[Symbol, T.untyped])]]) }
|
|
81
|
+
def unique_requirement_changes
|
|
82
|
+
previous_reqs = dependency.previous_requirements || []
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
changes = dependency.requirements.filter_map do |new_req|
|
|
85
|
+
old_req = previous_reqs.find { |r| r[:file] == new_req[:file] && r[:groups] == new_req[:groups] }
|
|
61
86
|
next if new_req == old_req
|
|
62
87
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
updated_requirement_or_setup_file_content(new_req, old_req)
|
|
66
|
-
next if updated_content == file.content
|
|
88
|
+
[new_req, old_req]
|
|
89
|
+
end
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
changes.uniq do |pair|
|
|
92
|
+
new_req = pair[0]
|
|
93
|
+
old_req = pair[1]
|
|
94
|
+
[new_req[:file], old_req&.fetch(:requirement), new_req.fetch(:requirement)]
|
|
70
95
|
end
|
|
71
96
|
end
|
|
72
97
|
|
|
73
98
|
sig do
|
|
74
99
|
params(
|
|
100
|
+
content: String,
|
|
75
101
|
new_req: T::Hash[Symbol, T.untyped],
|
|
76
102
|
old_req: T.nilable(T::Hash[Symbol, T.untyped])
|
|
77
103
|
).returns(String)
|
|
78
104
|
end
|
|
79
|
-
def updated_requirement_or_setup_file_content(new_req, old_req)
|
|
80
|
-
original_file = get_original_file(new_req.fetch(:file))
|
|
81
|
-
raise "Could not find a dependency file for #{new_req}" unless original_file
|
|
82
|
-
|
|
105
|
+
def updated_requirement_or_setup_file_content(content, new_req, old_req)
|
|
83
106
|
RequirementReplacer.new(
|
|
84
|
-
content:
|
|
107
|
+
content: content,
|
|
85
108
|
dependency_name: dependency.name,
|
|
86
109
|
old_requirement: old_req&.fetch(:requirement),
|
|
87
110
|
new_requirement: new_req.fetch(:requirement),
|
|
@@ -53,7 +53,7 @@ module Dependabot
|
|
|
53
53
|
# ignore it, since it isn't actually a declaration
|
|
54
54
|
next mtch if Regexp.last_match&.pre_match&.match?(/--.*\z/)
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
updated_matched_declaration(mtch)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
raise "Expected content to change!" if old_requirement != new_requirement && content == updated_content
|
|
@@ -98,29 +98,30 @@ module Dependabot
|
|
|
98
98
|
new_req_string
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
# Builds updated declaration from the actual matched text, preserving
|
|
102
|
+
# whatever extras (or lack thereof) appeared in the original match.
|
|
103
|
+
sig { params(matched_declaration: String).returns(String) }
|
|
104
|
+
def updated_matched_declaration(matched_declaration)
|
|
105
|
+
updated = if old_requirement
|
|
106
|
+
matched_declaration
|
|
107
|
+
.sub(RequirementParser::REQUIREMENTS, updated_requirement_string || "")
|
|
108
|
+
else
|
|
109
|
+
matched_declaration
|
|
110
|
+
.sub(RequirementParser::NAME_WITH_EXTRAS) { |nm| nm + (updated_requirement_string || "") }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return updated unless update_hashes? && matched_declaration.match?(RequirementParser::HASHES)
|
|
114
|
+
|
|
115
|
+
algorithm = T.must(matched_declaration.match(RequirementParser::HASHES))
|
|
116
|
+
.named_captures.fetch("algorithm")
|
|
117
|
+
separator = hash_separator(old_requirement)
|
|
118
|
+
updated.sub(
|
|
118
119
|
RequirementParser::HASHES,
|
|
119
120
|
package_hashes_for(
|
|
120
121
|
name: dependency_name,
|
|
121
122
|
version: new_hash_version,
|
|
122
|
-
algorithm:
|
|
123
|
-
).join(
|
|
123
|
+
algorithm: algorithm
|
|
124
|
+
).join(separator)
|
|
124
125
|
)
|
|
125
126
|
end
|
|
126
127
|
|
|
@@ -142,7 +143,14 @@ module Dependabot
|
|
|
142
143
|
def original_declaration_replacement_regex
|
|
143
144
|
original_string =
|
|
144
145
|
original_dependency_declaration_string(old_requirement)
|
|
145
|
-
|
|
146
|
+
match_data = T.must(original_string.match(RequirementParser::NAME_WITH_EXTRAS))
|
|
147
|
+
name_escaped = Regexp.escape(T.must(match_data[:name]))
|
|
148
|
+
# Everything after name+extras (the requirement/markers/hashes portion)
|
|
149
|
+
after_name_extras = T.must(original_string[T.must(match_data[0]).length..]).strip
|
|
150
|
+
after_escaped = Regexp.escape(after_name_extras)
|
|
151
|
+
# Match the dependency name with any extras (or none), followed by the requirement.
|
|
152
|
+
# This ensures a single gsub handles all extras variants of the same dependency.
|
|
153
|
+
/(?<![\-\w\.\[])#{name_escaped}\s*\\?\s*(?:\[[^\]]*\])?\s*\\?\s*#{after_escaped}(?![\-\w\.])/
|
|
146
154
|
end
|
|
147
155
|
|
|
148
156
|
sig { params(requirement: T.nilable(String)).returns(T::Boolean) }
|
|
@@ -64,7 +64,7 @@ module Dependabot
|
|
|
64
64
|
next unless first[:groups]
|
|
65
65
|
.include?("install_requires")
|
|
66
66
|
|
|
67
|
-
dep
|
|
67
|
+
name_with_extras(dep) + first[:requirement].to_s
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -76,7 +76,7 @@ module Dependabot
|
|
|
76
76
|
next unless first[:groups]
|
|
77
77
|
.include?("setup_requires")
|
|
78
78
|
|
|
79
|
-
dep
|
|
79
|
+
name_with_extras(dep) + first[:requirement].to_s
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
@@ -92,7 +92,7 @@ module Dependabot
|
|
|
92
92
|
|
|
93
93
|
hash[group.split(":").last] ||= []
|
|
94
94
|
hash[group.split(":").last] <<
|
|
95
|
-
(dep
|
|
95
|
+
(name_with_extras(dep) + first[:requirement].to_s)
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -100,6 +100,15 @@ module Dependabot
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# Reconstruct dependency name with extras from metadata.
|
|
104
|
+
sig { params(dep: Dependabot::Dependency).returns(String) }
|
|
105
|
+
def name_with_extras(dep)
|
|
106
|
+
extras_str = dep.metadata[:extras]
|
|
107
|
+
return dep.name unless extras_str.is_a?(String) && !extras_str.empty?
|
|
108
|
+
|
|
109
|
+
"#{dep.name}[#{extras_str}]"
|
|
110
|
+
end
|
|
111
|
+
|
|
103
112
|
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
104
113
|
def parsed_setup_file
|
|
105
114
|
@parsed_setup_file ||=
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "excon"
|
|
5
|
+
require "openssl"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
7
8
|
require "dependabot/metadata_finders"
|
|
@@ -27,6 +28,7 @@ module Dependabot
|
|
|
27
28
|
def initialize(dependency:, credentials:)
|
|
28
29
|
super
|
|
29
30
|
@pypi_listing = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
31
|
+
@parsed_source_urls = T.let({}, T::Hash[String, T.nilable(Dependabot::Source)])
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
sig { returns(T.nilable(String)) }
|
|
@@ -37,26 +39,88 @@ module Dependabot
|
|
|
37
39
|
super
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
sig { override.returns(T.nilable(String)) }
|
|
43
|
+
def maintainer_changes
|
|
44
|
+
return unless dependency.previous_version
|
|
45
|
+
return unless dependency.version
|
|
46
|
+
|
|
47
|
+
previous_ownership = ownership_for_version(T.must(dependency.previous_version))
|
|
48
|
+
current_ownership = ownership_for_version(T.must(dependency.version))
|
|
49
|
+
|
|
50
|
+
if previous_ownership.nil? || current_ownership.nil?
|
|
51
|
+
Dependabot.logger.info("Unable to determine ownership changes for #{dependency.name}")
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
previous_org = previous_ownership["organization"]
|
|
56
|
+
current_org = current_ownership["organization"]
|
|
57
|
+
|
|
58
|
+
if previous_org != current_org && !(previous_org.nil? && current_org)
|
|
59
|
+
return "The organization that maintains #{dependency.name} on PyPI has " \
|
|
60
|
+
"changed since your current version."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
previous_users = ownership_users(previous_ownership)
|
|
64
|
+
current_users = ownership_users(current_ownership)
|
|
65
|
+
|
|
66
|
+
# Warn only when there were previous maintainers and none of them remain
|
|
67
|
+
return unless previous_users.any? && !previous_users.intersect?(current_users)
|
|
68
|
+
|
|
69
|
+
"None of the maintainers for your current version of #{dependency.name} are " \
|
|
70
|
+
"listed as maintainers for the new version on PyPI."
|
|
71
|
+
end
|
|
72
|
+
|
|
40
73
|
private
|
|
41
74
|
|
|
42
75
|
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
43
76
|
def look_up_source
|
|
77
|
+
source_url = exact_match_source_url_from_project_urls
|
|
78
|
+
source_url ||= labelled_source_url_from_project_urls
|
|
79
|
+
source_url ||= fallback_source_url
|
|
80
|
+
source_url ||= source_from_description
|
|
81
|
+
source_url ||= source_from_homepage
|
|
82
|
+
|
|
83
|
+
parsed_source_from_url(source_url)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(T.nilable(String)) }
|
|
87
|
+
def exact_match_source_url_from_project_urls
|
|
88
|
+
project_urls.values.find do |url|
|
|
89
|
+
repo = parsed_source_from_url(url)&.repo
|
|
90
|
+
repo&.downcase&.end_with?(normalised_dependency_name)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { returns(T.nilable(String)) }
|
|
95
|
+
def labelled_source_url_from_project_urls
|
|
96
|
+
source_urls = source_like_project_url_labels.filter_map do |label|
|
|
97
|
+
project_urls[label]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
source_urls.find { |url| parsed_source_from_url(url) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { returns(T.nilable(String)) }
|
|
104
|
+
def fallback_source_url
|
|
44
105
|
potential_source_urls = [
|
|
45
|
-
pypi_listing.dig("info", "project_urls", "Source"),
|
|
46
|
-
pypi_listing.dig("info", "project_urls", "Repository"),
|
|
47
106
|
pypi_listing.dig("info", "home_page"),
|
|
48
107
|
pypi_listing.dig("info", "download_url"),
|
|
49
108
|
pypi_listing.dig("info", "docs_url")
|
|
50
109
|
].compact
|
|
51
110
|
|
|
52
|
-
potential_source_urls +=
|
|
53
|
-
(pypi_listing.dig("info", "project_urls") || {}).values
|
|
111
|
+
potential_source_urls += project_urls.values
|
|
54
112
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
source_url ||= source_from_homepage
|
|
113
|
+
potential_source_urls.find { |url| parsed_source_from_url(url) }
|
|
114
|
+
end
|
|
58
115
|
|
|
59
|
-
|
|
116
|
+
sig { returns(T::Hash[String, String]) }
|
|
117
|
+
def project_urls
|
|
118
|
+
pypi_listing.dig("info", "project_urls") || {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { returns(T::Array[String]) }
|
|
122
|
+
def source_like_project_url_labels
|
|
123
|
+
["Source", "Source Code", "Repository", "Code", "Homepage"]
|
|
60
124
|
end
|
|
61
125
|
|
|
62
126
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
@@ -73,7 +137,7 @@ module Dependabot
|
|
|
73
137
|
# Looking for a source where the repo name exactly matches the
|
|
74
138
|
# dependency name
|
|
75
139
|
match_url = potential_source_urls.find do |url|
|
|
76
|
-
repo =
|
|
140
|
+
repo = parsed_source_from_url(url)&.repo
|
|
77
141
|
repo&.downcase&.end_with?(normalised_dependency_name)
|
|
78
142
|
end
|
|
79
143
|
|
|
@@ -83,11 +147,14 @@ module Dependabot
|
|
|
83
147
|
# mentioned when the link is followed
|
|
84
148
|
@source_from_description ||= T.let(
|
|
85
149
|
potential_source_urls.find do |url|
|
|
86
|
-
full_url =
|
|
150
|
+
full_url = parsed_source_from_url(url)&.url
|
|
87
151
|
next unless full_url
|
|
88
152
|
|
|
89
153
|
response = Dependabot::RegistryClient.get(url: full_url)
|
|
90
|
-
|
|
154
|
+
unless response.status == 200
|
|
155
|
+
Dependabot.logger.warn("Error fetching source URL #{full_url}: HTTP #{response.status}")
|
|
156
|
+
next
|
|
157
|
+
end
|
|
91
158
|
|
|
92
159
|
response.body.include?(normalised_dependency_name)
|
|
93
160
|
end,
|
|
@@ -108,7 +175,7 @@ module Dependabot
|
|
|
108
175
|
end
|
|
109
176
|
|
|
110
177
|
match_url = potential_source_urls.find do |url|
|
|
111
|
-
repo =
|
|
178
|
+
repo = parsed_source_from_url(url)&.repo
|
|
112
179
|
repo&.downcase&.end_with?(normalised_dependency_name)
|
|
113
180
|
end
|
|
114
181
|
|
|
@@ -116,7 +183,7 @@ module Dependabot
|
|
|
116
183
|
|
|
117
184
|
@source_from_homepage ||= T.let(
|
|
118
185
|
potential_source_urls.find do |url|
|
|
119
|
-
full_url =
|
|
186
|
+
full_url = parsed_source_from_url(url)&.url
|
|
120
187
|
next unless full_url
|
|
121
188
|
|
|
122
189
|
response = Dependabot::RegistryClient.get(url: full_url)
|
|
@@ -143,7 +210,8 @@ module Dependabot
|
|
|
143
210
|
begin
|
|
144
211
|
Dependabot::RegistryClient.get(url: homepage_url)
|
|
145
212
|
rescue Excon::Error::Timeout, Excon::Error::Socket,
|
|
146
|
-
Excon::Error::TooManyRedirects, ArgumentError
|
|
213
|
+
Excon::Error::TooManyRedirects, OpenSSL::SSL::SSLError, ArgumentError => e
|
|
214
|
+
Dependabot.logger.warn("Error fetching Python homepage URL #{homepage_url}: #{e.class}: #{e.message}")
|
|
147
215
|
nil
|
|
148
216
|
end,
|
|
149
217
|
T.nilable(Excon::Response)
|
|
@@ -165,9 +233,8 @@ module Dependabot
|
|
|
165
233
|
|
|
166
234
|
@pypi_listing = JSON.parse(response.body)
|
|
167
235
|
return @pypi_listing
|
|
168
|
-
rescue JSON::ParserError
|
|
169
|
-
|
|
170
|
-
rescue Excon::Error::Timeout
|
|
236
|
+
rescue JSON::ParserError, Excon::Error::Timeout, Excon::Error::Socket, OpenSSL::SSL::SSLError => e
|
|
237
|
+
Dependabot.logger.warn("Error fetching Python package listing from #{url}: #{e.class}: #{e.message}")
|
|
171
238
|
next
|
|
172
239
|
end
|
|
173
240
|
|
|
@@ -194,23 +261,88 @@ module Dependabot
|
|
|
194
261
|
|
|
195
262
|
sig { returns(T::Array[String]) }
|
|
196
263
|
def possible_listing_urls
|
|
197
|
-
|
|
264
|
+
index_credentials =
|
|
198
265
|
credentials
|
|
199
266
|
.select { |cred| cred["type"] == "python_index" }
|
|
200
|
-
.map { |c| AuthedUrlBuilder.authed_url(credential: c) }
|
|
201
267
|
|
|
202
|
-
|
|
268
|
+
credential_urls = index_credentials
|
|
269
|
+
.map { |c| AuthedUrlBuilder.authed_url(credential: c) }
|
|
270
|
+
.reject { |url| url.strip.empty? }
|
|
271
|
+
|
|
272
|
+
base_urls = if index_credentials.any?(&:replaces_base?)
|
|
273
|
+
credential_urls
|
|
274
|
+
else
|
|
275
|
+
credential_urls + [MAIN_PYPI_URL]
|
|
276
|
+
end
|
|
277
|
+
base_urls = [MAIN_PYPI_URL] if base_urls.empty?
|
|
278
|
+
|
|
279
|
+
base_urls.map do |base_url|
|
|
203
280
|
# Convert /simple/ endpoints to /pypi/ for JSON API access
|
|
204
281
|
json_base_url = base_url.sub(%r{/simple/?$}i, "/pypi")
|
|
205
282
|
json_base_url.gsub(%r{/$}, "") + "/#{normalised_dependency_name}/json"
|
|
206
283
|
end
|
|
207
284
|
end
|
|
208
285
|
|
|
286
|
+
sig { params(version: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
287
|
+
def ownership_for_version(version)
|
|
288
|
+
if version.include?("+")
|
|
289
|
+
Dependabot.logger.info("Version #{version} includes a local version identifier, skipping ownership check")
|
|
290
|
+
return nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
possible_version_listing_urls(version).each do |url|
|
|
294
|
+
response = fetch_authed_url(url)
|
|
295
|
+
unless response.status == 200
|
|
296
|
+
Dependabot.logger.warn(
|
|
297
|
+
"Error fetching Python package ownership from #{url} for version #{version}: " \
|
|
298
|
+
"HTTP #{response.status}"
|
|
299
|
+
)
|
|
300
|
+
next
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
data = JSON.parse(response.body)
|
|
304
|
+
ownership = data["ownership"]
|
|
305
|
+
Dependabot.logger.debug("Found ownership for #{dependency.name} version #{version}")
|
|
306
|
+
return ownership
|
|
307
|
+
rescue JSON::ParserError, Excon::Error::Timeout, Excon::Error::Socket,
|
|
308
|
+
Excon::Error::TooManyRedirects, OpenSSL::SSL::SSLError, ArgumentError => e
|
|
309
|
+
Dependabot.logger.warn(
|
|
310
|
+
"Error fetching Python package ownership from #{url} for version #{version}: #{e.class}: #{e.message}"
|
|
311
|
+
)
|
|
312
|
+
next
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
sig { params(version: String).returns(T::Array[String]) }
|
|
319
|
+
def possible_version_listing_urls(version)
|
|
320
|
+
possible_listing_urls.map do |url|
|
|
321
|
+
url.sub(%r{/json$}, "/#{URI::DEFAULT_PARSER.escape(version)}/json")
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
sig { params(ownership: T::Hash[String, T.untyped]).returns(T::Array[String]) }
|
|
326
|
+
def ownership_users(ownership)
|
|
327
|
+
roles = ownership["roles"]
|
|
328
|
+
return [] unless roles.is_a?(Array)
|
|
329
|
+
|
|
330
|
+
roles.filter_map { |role| role["user"] if role.is_a?(Hash) }
|
|
331
|
+
end
|
|
332
|
+
|
|
209
333
|
# Strip [extras] from name (dependency_name[extra_dep,other_extra])
|
|
210
334
|
sig { returns(String) }
|
|
211
335
|
def normalised_dependency_name
|
|
212
336
|
NameNormaliser.normalise(dependency.name)
|
|
213
337
|
end
|
|
338
|
+
|
|
339
|
+
sig { params(url: T.nilable(String)).returns(T.nilable(Dependabot::Source)) }
|
|
340
|
+
def parsed_source_from_url(url)
|
|
341
|
+
return unless url
|
|
342
|
+
return @parsed_source_urls[url] if @parsed_source_urls.key?(url)
|
|
343
|
+
|
|
344
|
+
@parsed_source_urls[url] = Source.from_url(url)
|
|
345
|
+
end
|
|
214
346
|
end
|
|
215
347
|
end
|
|
216
348
|
end
|
|
@@ -238,7 +238,8 @@ module Dependabot
|
|
|
238
238
|
content = file.content
|
|
239
239
|
return [] if content.nil?
|
|
240
240
|
|
|
241
|
-
paths = content.scan(CHILD_REQUIREMENT_REGEX).flatten
|
|
241
|
+
paths = content.scan(CHILD_REQUIREMENT_REGEX).flatten +
|
|
242
|
+
content.scan(CONSTRAINT_REGEX).flatten
|
|
242
243
|
current_dir = File.dirname(file.name)
|
|
243
244
|
|
|
244
245
|
paths.flat_map do |path|
|
|
@@ -268,7 +269,8 @@ module Dependabot
|
|
|
268
269
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
269
270
|
def constraints_files
|
|
270
271
|
all_requirement_files = requirements_txt_files +
|
|
271
|
-
child_requirement_txt_files
|
|
272
|
+
child_requirement_txt_files +
|
|
273
|
+
requirements_in_files
|
|
272
274
|
|
|
273
275
|
constraints_paths = all_requirement_files.map do |req_file|
|
|
274
276
|
current_dir = File.dirname(req_file.name)
|
|
@@ -283,7 +285,10 @@ module Dependabot
|
|
|
283
285
|
end
|
|
284
286
|
end.flatten.uniq
|
|
285
287
|
|
|
286
|
-
|
|
288
|
+
already_fetched_names = child_requirement_files.map(&:name)
|
|
289
|
+
constraints_paths
|
|
290
|
+
.reject { |path| already_fetched_names.include?(path) }
|
|
291
|
+
.map { |path| fetch_file_from_host(path) }
|
|
287
292
|
end
|
|
288
293
|
|
|
289
294
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
@@ -355,7 +360,7 @@ module Dependabot
|
|
|
355
360
|
|
|
356
361
|
uneditable_reqs =
|
|
357
362
|
content
|
|
358
|
-
.scan(/(?<name>^['"]?(?:file:)?(?<path>\.[^\[#'"\n]*))/)
|
|
363
|
+
.scan(/(?<name>^['"]?(?:file:)?(?<path>\.[^\[#'"\n;]*))/)
|
|
359
364
|
.filter_map do |match_array|
|
|
360
365
|
n, p = match_array
|
|
361
366
|
{ name: n.to_s.strip, path: p.to_s.strip, file: req_file.name } unless p.to_s.include?("://")
|
|
@@ -363,7 +368,7 @@ module Dependabot
|
|
|
363
368
|
|
|
364
369
|
editable_reqs =
|
|
365
370
|
content
|
|
366
|
-
.scan(/(?<name>^-e\s+['"]?(?:file:)?(?<path>[^\[#'"\n]*))/)
|
|
371
|
+
.scan(/(?<name>^-e\s+['"]?(?:file:)?(?<path>[^\[#'"\n;]*))/)
|
|
367
372
|
.filter_map do |match_array|
|
|
368
373
|
n, p = match_array
|
|
369
374
|
unless p.to_s.include?("://") || p.to_s.include?("git@")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-python
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.368.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.368.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.
|
|
25
|
+
version: 0.368.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -250,6 +250,7 @@ files:
|
|
|
250
250
|
- lib/dependabot/python.rb
|
|
251
251
|
- lib/dependabot/python/authed_url_builder.rb
|
|
252
252
|
- lib/dependabot/python/dependency_grapher.rb
|
|
253
|
+
- lib/dependabot/python/dependency_grapher/lockfile_generator.rb
|
|
253
254
|
- lib/dependabot/python/file_fetcher.rb
|
|
254
255
|
- lib/dependabot/python/file_parser.rb
|
|
255
256
|
- lib/dependabot/python/file_parser/pipfile_files_parser.rb
|
|
@@ -293,7 +294,7 @@ licenses:
|
|
|
293
294
|
- MIT
|
|
294
295
|
metadata:
|
|
295
296
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
296
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
297
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.368.0
|
|
297
298
|
rdoc_options: []
|
|
298
299
|
require_paths:
|
|
299
300
|
- lib
|