dependabot-python 0.79.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 +7 -0
- data/helpers/build +17 -0
- data/helpers/lib/__init__.py +0 -0
- data/helpers/lib/hasher.py +23 -0
- data/helpers/lib/parser.py +130 -0
- data/helpers/requirements.txt +9 -0
- data/helpers/run.py +18 -0
- data/lib/dependabot/python.rb +11 -0
- data/lib/dependabot/python/file_fetcher.rb +307 -0
- data/lib/dependabot/python/file_parser.rb +221 -0
- data/lib/dependabot/python/file_parser/pipfile_files_parser.rb +150 -0
- data/lib/dependabot/python/file_parser/poetry_files_parser.rb +139 -0
- data/lib/dependabot/python/file_parser/setup_file_parser.rb +158 -0
- data/lib/dependabot/python/file_updater.rb +149 -0
- data/lib/dependabot/python/file_updater/pip_compile_file_updater.rb +361 -0
- data/lib/dependabot/python/file_updater/pipfile_file_updater.rb +391 -0
- data/lib/dependabot/python/file_updater/pipfile_preparer.rb +123 -0
- data/lib/dependabot/python/file_updater/poetry_file_updater.rb +282 -0
- data/lib/dependabot/python/file_updater/pyproject_preparer.rb +103 -0
- data/lib/dependabot/python/file_updater/requirement_file_updater.rb +160 -0
- data/lib/dependabot/python/file_updater/requirement_replacer.rb +93 -0
- data/lib/dependabot/python/file_updater/setup_file_sanitizer.rb +89 -0
- data/lib/dependabot/python/metadata_finder.rb +122 -0
- data/lib/dependabot/python/native_helpers.rb +17 -0
- data/lib/dependabot/python/python_versions.rb +25 -0
- data/lib/dependabot/python/requirement.rb +129 -0
- data/lib/dependabot/python/requirement_parser.rb +38 -0
- data/lib/dependabot/python/update_checker.rb +229 -0
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +250 -0
- data/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +379 -0
- data/lib/dependabot/python/update_checker/pipfile_version_resolver.rb +558 -0
- data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +298 -0
- data/lib/dependabot/python/update_checker/requirements_updater.rb +365 -0
- data/lib/dependabot/python/version.rb +87 -0
- metadata +203 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/dependency"
|
4
|
+
require "dependabot/errors"
|
5
|
+
require "dependabot/file_parsers/base/dependency_set"
|
6
|
+
require "dependabot/shared_helpers"
|
7
|
+
require "dependabot/python/file_parser"
|
8
|
+
require "dependabot/python/native_helpers"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Python
|
12
|
+
class FileParser
|
13
|
+
class SetupFileParser
|
14
|
+
INSTALL_REQUIRES_REGEX =
|
15
|
+
/install_requires\s*=\s*(\[.*?\])[,)\s]/m.freeze
|
16
|
+
SETUP_REQUIRES_REGEX = /setup_requires\s*=\s*(\[.*?\])[,)\s]/m.freeze
|
17
|
+
TESTS_REQUIRE_REGEX = /tests_require\s*=\s*(\[.*?\])[,)\s]/m.freeze
|
18
|
+
EXTRAS_REQUIRE_REGEX = /extras_require\s*=\s*(\{.*?\})[,)\s]/m.freeze
|
19
|
+
|
20
|
+
def initialize(dependency_files:)
|
21
|
+
@dependency_files = dependency_files
|
22
|
+
end
|
23
|
+
|
24
|
+
def dependency_set
|
25
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
26
|
+
|
27
|
+
parsed_setup_file.each do |dep|
|
28
|
+
# If a requirement has a `<` or `<=` marker then updating it is
|
29
|
+
# probably blocked. Ignore it.
|
30
|
+
next if dep["markers"].include?("<")
|
31
|
+
|
32
|
+
# If the requirement is our inserted version, ignore it
|
33
|
+
# (we wouldn't be able to update it)
|
34
|
+
next if dep["version"] == "0.0.1+dependabot"
|
35
|
+
|
36
|
+
dependencies <<
|
37
|
+
Dependency.new(
|
38
|
+
name: normalised_name(dep["name"]),
|
39
|
+
version: dep["version"]&.include?("*") ? nil : dep["version"],
|
40
|
+
requirements: [{
|
41
|
+
requirement: dep["requirement"],
|
42
|
+
file: Pathname.new(dep["file"]).cleanpath.to_path,
|
43
|
+
source: nil,
|
44
|
+
groups: [dep["requirement_type"]]
|
45
|
+
}],
|
46
|
+
package_manager: "pip"
|
47
|
+
)
|
48
|
+
end
|
49
|
+
dependencies
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :dependency_files
|
55
|
+
|
56
|
+
def parsed_setup_file
|
57
|
+
SharedHelpers.in_a_temporary_directory do
|
58
|
+
write_temporary_dependency_files
|
59
|
+
|
60
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
61
|
+
command: "pyenv exec python #{NativeHelpers.python_helper_path}",
|
62
|
+
function: "parse_setup",
|
63
|
+
args: [Dir.pwd]
|
64
|
+
)
|
65
|
+
|
66
|
+
check_requirements(requirements)
|
67
|
+
requirements
|
68
|
+
end
|
69
|
+
rescue SharedHelpers::HelperSubprocessFailed => error
|
70
|
+
if error.message.start_with?("InstallationError")
|
71
|
+
raise Dependabot::DependencyFileNotEvaluatable, error.message
|
72
|
+
end
|
73
|
+
|
74
|
+
parsed_sanitized_setup_file
|
75
|
+
end
|
76
|
+
|
77
|
+
def parsed_sanitized_setup_file
|
78
|
+
SharedHelpers.in_a_temporary_directory do
|
79
|
+
write_sanitized_setup_file
|
80
|
+
|
81
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
82
|
+
command: "pyenv exec python #{NativeHelpers.python_helper_path}",
|
83
|
+
function: "parse_setup",
|
84
|
+
args: [Dir.pwd]
|
85
|
+
)
|
86
|
+
|
87
|
+
check_requirements(requirements)
|
88
|
+
requirements
|
89
|
+
end
|
90
|
+
rescue SharedHelpers::HelperSubprocessFailed
|
91
|
+
# Assume there are no dependencies in setup.py files that fail to
|
92
|
+
# parse. This isn't ideal, and we should continue to improve
|
93
|
+
# parsing, but there are a *lot* of things that can go wrong at
|
94
|
+
# the moment!
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_requirements(requirements)
|
99
|
+
requirements.each do |dep|
|
100
|
+
next unless dep["requirement"]
|
101
|
+
|
102
|
+
Python::Requirement.new(dep["requirement"].split(","))
|
103
|
+
rescue Gem::Requirement::BadRequirementError => error
|
104
|
+
raise Dependabot::DependencyFileNotEvaluatable, error.message
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def write_temporary_dependency_files
|
109
|
+
dependency_files.
|
110
|
+
reject { |f| f.name == ".python-version" }.
|
111
|
+
each do |file|
|
112
|
+
path = file.name
|
113
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
114
|
+
File.write(path, file.content)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Write a setup.py with only entries for the requires fields.
|
119
|
+
#
|
120
|
+
# This sanitization is far from perfect (it will fail if any of the
|
121
|
+
# entries are dynamic), but it is an alternative approach to the one
|
122
|
+
# used in parser.py which sometimes succeeds when that has failed.
|
123
|
+
def write_sanitized_setup_file
|
124
|
+
original_content = setup_file.content
|
125
|
+
|
126
|
+
install_requires =
|
127
|
+
original_content.match(INSTALL_REQUIRES_REGEX)&.captures&.first
|
128
|
+
setup_requires =
|
129
|
+
original_content.match(SETUP_REQUIRES_REGEX)&.captures&.first
|
130
|
+
tests_require =
|
131
|
+
original_content.match(TESTS_REQUIRE_REGEX)&.captures&.first
|
132
|
+
extras_require =
|
133
|
+
original_content.match(EXTRAS_REQUIRE_REGEX)&.captures&.first
|
134
|
+
|
135
|
+
tmp = "from setuptools import setup\n\n"\
|
136
|
+
"setup(name=\"sanitized-package\",version=\"0.0.1\","
|
137
|
+
|
138
|
+
tmp += "install_requires=#{install_requires}," if install_requires
|
139
|
+
tmp += "setup_requires=#{setup_requires}," if setup_requires
|
140
|
+
tmp += "tests_require=#{tests_require}," if tests_require
|
141
|
+
tmp += "extras_require=#{extras_require}," if extras_require
|
142
|
+
tmp += ")"
|
143
|
+
|
144
|
+
File.write("setup.py", tmp)
|
145
|
+
end
|
146
|
+
|
147
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
148
|
+
def normalised_name(name)
|
149
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
150
|
+
end
|
151
|
+
|
152
|
+
def setup_file
|
153
|
+
dependency_files.find { |f| f.name == "setup.py" }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/file_updaters"
|
4
|
+
require "dependabot/file_updaters/base"
|
5
|
+
require "dependabot/shared_helpers"
|
6
|
+
|
7
|
+
module Dependabot
|
8
|
+
module Python
|
9
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
10
|
+
require_relative "file_updater/pipfile_file_updater"
|
11
|
+
require_relative "file_updater/pip_compile_file_updater"
|
12
|
+
require_relative "file_updater/poetry_file_updater"
|
13
|
+
require_relative "file_updater/requirement_file_updater"
|
14
|
+
|
15
|
+
def self.updated_files_regex
|
16
|
+
[
|
17
|
+
/^Pipfile$/,
|
18
|
+
/^Pipfile\.lock$/,
|
19
|
+
/.*\.txt$/,
|
20
|
+
/.*\.in$/,
|
21
|
+
/^setup\.py$/,
|
22
|
+
/^pyproject\.toml$/,
|
23
|
+
/^pyproject\.lock$/
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def updated_dependency_files
|
28
|
+
updated_files =
|
29
|
+
case resolver_type
|
30
|
+
when :pipfile then updated_pipfile_based_files
|
31
|
+
when :poetry then updated_poetry_based_files
|
32
|
+
when :pip_compile then updated_pip_compile_based_files
|
33
|
+
when :requirements then updated_requirement_based_files
|
34
|
+
else raise "Unexpected resolver type: #{resolver_type}"
|
35
|
+
end
|
36
|
+
|
37
|
+
if updated_files.none? ||
|
38
|
+
updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
|
39
|
+
raise "No files have changed!"
|
40
|
+
end
|
41
|
+
|
42
|
+
updated_files
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def resolver_type
|
48
|
+
reqs = dependencies.flat_map(&:requirements)
|
49
|
+
req_files = reqs.map { |r| r.fetch(:file) }
|
50
|
+
|
51
|
+
# If there are no requirements then this is a sub-dependency. It
|
52
|
+
# must come from one of Pipenv, Poetry or pip-tools, and can't come
|
53
|
+
# from the first two unless they have a lockfile.
|
54
|
+
return subdependency_resolver if reqs.none?
|
55
|
+
|
56
|
+
# Otherwise, this is a top-level dependency, and we can figure out
|
57
|
+
# which resolver to use based on the filename of its requirements
|
58
|
+
return :pipfile if req_files.any? { |f| f == "Pipfile" }
|
59
|
+
return :poetry if req_files.any? { |f| f == "pyproject.toml" }
|
60
|
+
return :pip_compile if req_files.any? { |f| f.end_with?(".in") }
|
61
|
+
|
62
|
+
# Finally, we should only ever be updating a requirements.txt file if
|
63
|
+
# some requirements have changed. Otherwise, this must be a case where
|
64
|
+
# we have a requirements.txt *and* some other resolver of which the
|
65
|
+
# dependency is a sub-dependency.
|
66
|
+
changed_reqs = reqs - dependencies.flat_map(&:previous_requirements)
|
67
|
+
changed_reqs.none? ? subdependency_resolver : :requirements
|
68
|
+
end
|
69
|
+
|
70
|
+
def subdependency_resolver
|
71
|
+
return :pipfile if pipfile_lock
|
72
|
+
return :poetry if poetry_lock || pyproject_lock
|
73
|
+
return :pip_compile if pip_compile_files.any?
|
74
|
+
|
75
|
+
raise "Claimed to be a sub-dependency, but no lockfile exists!"
|
76
|
+
end
|
77
|
+
|
78
|
+
def updated_pipfile_based_files
|
79
|
+
PipfileFileUpdater.new(
|
80
|
+
dependencies: dependencies,
|
81
|
+
dependency_files: dependency_files,
|
82
|
+
credentials: credentials
|
83
|
+
).updated_dependency_files
|
84
|
+
end
|
85
|
+
|
86
|
+
def updated_poetry_based_files
|
87
|
+
PoetryFileUpdater.new(
|
88
|
+
dependencies: dependencies,
|
89
|
+
dependency_files: dependency_files,
|
90
|
+
credentials: credentials
|
91
|
+
).updated_dependency_files
|
92
|
+
end
|
93
|
+
|
94
|
+
def updated_pip_compile_based_files
|
95
|
+
PipCompileFileUpdater.new(
|
96
|
+
dependencies: dependencies,
|
97
|
+
dependency_files: dependency_files,
|
98
|
+
credentials: credentials
|
99
|
+
).updated_dependency_files
|
100
|
+
end
|
101
|
+
|
102
|
+
def updated_requirement_based_files
|
103
|
+
RequirementFileUpdater.new(
|
104
|
+
dependencies: dependencies,
|
105
|
+
dependency_files: dependency_files,
|
106
|
+
credentials: credentials
|
107
|
+
).updated_dependency_files
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_required_files
|
111
|
+
filenames = dependency_files.map(&:name)
|
112
|
+
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
|
113
|
+
return if pipfile
|
114
|
+
return if pyproject
|
115
|
+
return if get_original_file("setup.py")
|
116
|
+
|
117
|
+
raise "No requirements.txt or setup.py!"
|
118
|
+
end
|
119
|
+
|
120
|
+
def pipfile
|
121
|
+
@pipfile ||= get_original_file("Pipfile")
|
122
|
+
end
|
123
|
+
|
124
|
+
def pipfile_lock
|
125
|
+
@pipfile_lock ||= get_original_file("Pipfile.lock")
|
126
|
+
end
|
127
|
+
|
128
|
+
def pyproject
|
129
|
+
@pyproject ||= get_original_file("pyproject.toml")
|
130
|
+
end
|
131
|
+
|
132
|
+
def pyproject_lock
|
133
|
+
@pyproject_lock ||= get_original_file("pyproject.lock")
|
134
|
+
end
|
135
|
+
|
136
|
+
def poetry_lock
|
137
|
+
@poetry_lock ||= get_original_file("poetry.lock")
|
138
|
+
end
|
139
|
+
|
140
|
+
def pip_compile_files
|
141
|
+
@pip_compile_files ||=
|
142
|
+
dependency_files.select { |f| f.name.end_with?(".in") }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
Dependabot::FileUpdaters.
|
149
|
+
register("pip", Dependabot::Python::FileUpdater)
|
@@ -0,0 +1,361 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/python/requirement_parser"
|
4
|
+
require "dependabot/python/file_fetcher"
|
5
|
+
require "dependabot/python/file_updater"
|
6
|
+
require "dependabot/shared_helpers"
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
module Dependabot
|
10
|
+
module Python
|
11
|
+
class FileUpdater
|
12
|
+
class PipCompileFileUpdater
|
13
|
+
require_relative "requirement_replacer"
|
14
|
+
require_relative "requirement_file_updater"
|
15
|
+
require_relative "setup_file_sanitizer"
|
16
|
+
|
17
|
+
UNSAFE_PACKAGES = %w(setuptools distribute pip).freeze
|
18
|
+
|
19
|
+
attr_reader :dependencies, :dependency_files, :credentials
|
20
|
+
|
21
|
+
def initialize(dependencies:, dependency_files:, credentials:)
|
22
|
+
@dependencies = dependencies
|
23
|
+
@dependency_files = dependency_files
|
24
|
+
@credentials = credentials
|
25
|
+
end
|
26
|
+
|
27
|
+
def updated_dependency_files
|
28
|
+
return @updated_dependency_files if @update_already_attempted
|
29
|
+
|
30
|
+
@update_already_attempted = true
|
31
|
+
@updated_dependency_files ||= fetch_updated_dependency_files
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def dependency
|
37
|
+
# For now, we'll only ever be updating a single dependency
|
38
|
+
dependencies.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_updated_dependency_files
|
42
|
+
updated_compiled_files = compile_new_requirement_files
|
43
|
+
updated_manifest_files = update_manifest_files
|
44
|
+
|
45
|
+
updated_files = updated_compiled_files + updated_manifest_files
|
46
|
+
updated_uncompiled_files = update_uncompiled_files(updated_files)
|
47
|
+
|
48
|
+
[
|
49
|
+
*updated_manifest_files,
|
50
|
+
*updated_compiled_files,
|
51
|
+
*updated_uncompiled_files
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
def compile_new_requirement_files
|
56
|
+
SharedHelpers.in_a_temporary_directory do
|
57
|
+
write_updated_dependency_files
|
58
|
+
|
59
|
+
filenames_to_compile.each do |filename|
|
60
|
+
# Shell out to pip-compile, generate a new set of requirements.
|
61
|
+
# This is slow, as pip-compile needs to do installs.
|
62
|
+
run_command(
|
63
|
+
"pyenv exec pip-compile #{pip_compile_options(filename)} "\
|
64
|
+
"-P #{dependency.name} #{filename}"
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
dependency_files.map do |file|
|
69
|
+
next unless file.name.end_with?(".txt")
|
70
|
+
|
71
|
+
updated_content = File.read(file.name)
|
72
|
+
|
73
|
+
updated_content =
|
74
|
+
replace_header_with_original(updated_content, file.content)
|
75
|
+
next if updated_content == file.content
|
76
|
+
|
77
|
+
file.dup.tap { |f| f.content = updated_content }
|
78
|
+
end.compact
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_manifest_files
|
83
|
+
dependency_files.map do |file|
|
84
|
+
next unless file.name.end_with?(".in")
|
85
|
+
|
86
|
+
file = file.dup
|
87
|
+
updated_content = update_dependency_requirement(file)
|
88
|
+
next if updated_content == file.content
|
89
|
+
|
90
|
+
file.content = updated_content
|
91
|
+
file
|
92
|
+
end.compact
|
93
|
+
end
|
94
|
+
|
95
|
+
def update_uncompiled_files(updated_files)
|
96
|
+
updated_filenames = updated_files.map(&:name)
|
97
|
+
old_reqs = dependency.previous_requirements.
|
98
|
+
reject { |r| updated_filenames.include?(r[:file]) }
|
99
|
+
new_reqs = dependency.requirements.
|
100
|
+
reject { |r| updated_filenames.include?(r[:file]) }
|
101
|
+
|
102
|
+
return [] if new_reqs.none?
|
103
|
+
|
104
|
+
files = dependency_files.
|
105
|
+
reject { |file| updated_filenames.include?(file.name) }
|
106
|
+
|
107
|
+
args = dependency.to_h
|
108
|
+
args = Hash[args.keys.map { |k| [k.to_sym, args[k]] }]
|
109
|
+
args[:requirements] = new_reqs
|
110
|
+
args[:previous_requirements] = old_reqs
|
111
|
+
|
112
|
+
RequirementFileUpdater.new(
|
113
|
+
dependencies: [Dependency.new(**args)],
|
114
|
+
dependency_files: files,
|
115
|
+
credentials: credentials
|
116
|
+
).updated_dependency_files
|
117
|
+
end
|
118
|
+
|
119
|
+
def run_command(command)
|
120
|
+
command = command.dup
|
121
|
+
raw_response = nil
|
122
|
+
IO.popen(command, err: %i(child out)) do |process|
|
123
|
+
raw_response = process.read
|
124
|
+
end
|
125
|
+
|
126
|
+
# Raise an error with the output from the shell session if
|
127
|
+
# pip-compile returns a non-zero status
|
128
|
+
return if $CHILD_STATUS.success?
|
129
|
+
|
130
|
+
raise SharedHelpers::HelperSubprocessFailed.new(
|
131
|
+
raw_response,
|
132
|
+
command
|
133
|
+
)
|
134
|
+
rescue SharedHelpers::HelperSubprocessFailed => error
|
135
|
+
original_error ||= error
|
136
|
+
msg = error.message
|
137
|
+
|
138
|
+
relevant_error =
|
139
|
+
if error_suggests_bad_python_version?(msg) then original_error
|
140
|
+
else error
|
141
|
+
end
|
142
|
+
|
143
|
+
raise relevant_error unless error_suggests_bad_python_version?(msg)
|
144
|
+
raise relevant_error if File.exist?(".python-version")
|
145
|
+
|
146
|
+
command = "pyenv local 2.7.15 && " + command
|
147
|
+
retry
|
148
|
+
ensure
|
149
|
+
FileUtils.remove_entry(".python-version", true)
|
150
|
+
end
|
151
|
+
|
152
|
+
def error_suggests_bad_python_version?(message)
|
153
|
+
return true if message.include?("not find a version that satisfies")
|
154
|
+
|
155
|
+
message.include?('Command "python setup.py egg_info" failed')
|
156
|
+
end
|
157
|
+
|
158
|
+
def write_updated_dependency_files
|
159
|
+
dependency_files.each do |file|
|
160
|
+
next if file.name == ".python-version"
|
161
|
+
|
162
|
+
path = file.name
|
163
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
164
|
+
File.write(path, freeze_dependency_requirement(file))
|
165
|
+
end
|
166
|
+
|
167
|
+
setup_files.each do |file|
|
168
|
+
path = file.name
|
169
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
170
|
+
File.write(path, sanitized_setup_file_content(file))
|
171
|
+
end
|
172
|
+
|
173
|
+
setup_cfg_files.each do |file|
|
174
|
+
path = file.name
|
175
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
176
|
+
File.write(path, "[metadata]\nname = sanitized-package\n")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def sanitized_setup_file_content(file)
|
181
|
+
@sanitized_setup_file_content ||= {}
|
182
|
+
if @sanitized_setup_file_content[file.name]
|
183
|
+
return @sanitized_setup_file_content[file.name]
|
184
|
+
end
|
185
|
+
|
186
|
+
@sanitized_setup_file_content[file.name] =
|
187
|
+
SetupFileSanitizer.
|
188
|
+
new(setup_file: file, setup_cfg: setup_cfg(file)).
|
189
|
+
sanitized_content
|
190
|
+
end
|
191
|
+
|
192
|
+
def setup_cfg(file)
|
193
|
+
dependency_files.find do |f|
|
194
|
+
f.name == file.name.sub(/\.py$/, ".cfg")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def freeze_dependency_requirement(file)
|
199
|
+
return file.content unless file.name.end_with?(".in")
|
200
|
+
|
201
|
+
old_req = dependency.previous_requirements.
|
202
|
+
find { |r| r[:file] == file.name }
|
203
|
+
|
204
|
+
return file.content unless old_req
|
205
|
+
return file.content if old_req == "==#{dependency.version}"
|
206
|
+
|
207
|
+
RequirementReplacer.new(
|
208
|
+
content: file.content,
|
209
|
+
dependency_name: dependency.name,
|
210
|
+
old_requirement: old_req[:requirement],
|
211
|
+
new_requirement: "==#{dependency.version}"
|
212
|
+
).updated_content
|
213
|
+
end
|
214
|
+
|
215
|
+
def update_dependency_requirement(file)
|
216
|
+
return file.content unless file.name.end_with?(".in")
|
217
|
+
|
218
|
+
old_req = dependency.previous_requirements.
|
219
|
+
find { |r| r[:file] == file.name }
|
220
|
+
new_req = dependency.requirements.
|
221
|
+
find { |r| r[:file] == file.name }
|
222
|
+
return file.content unless old_req&.fetch(:requirement)
|
223
|
+
return file.content if old_req == new_req
|
224
|
+
|
225
|
+
RequirementReplacer.new(
|
226
|
+
content: file.content,
|
227
|
+
dependency_name: dependency.name,
|
228
|
+
old_requirement: old_req[:requirement],
|
229
|
+
new_requirement: new_req[:requirement]
|
230
|
+
).updated_content
|
231
|
+
end
|
232
|
+
|
233
|
+
def replace_header_with_original(updated_content, original_content)
|
234
|
+
original_header_lines =
|
235
|
+
original_content.lines.take_while { |l| l.start_with?("#") }
|
236
|
+
|
237
|
+
updated_content_lines =
|
238
|
+
updated_content.lines.drop_while { |l| l.start_with?("#") }
|
239
|
+
|
240
|
+
[*original_header_lines, *updated_content_lines].join
|
241
|
+
end
|
242
|
+
|
243
|
+
def pip_compile_options(filename)
|
244
|
+
current_requirements_file_name = filename.sub(/\.in$/, ".txt")
|
245
|
+
|
246
|
+
requirements_file =
|
247
|
+
dependency_files.
|
248
|
+
find { |f| f.name == current_requirements_file_name }
|
249
|
+
|
250
|
+
return unless requirements_file
|
251
|
+
|
252
|
+
options = ""
|
253
|
+
|
254
|
+
if requirements_file.content.include?("--hash=sha")
|
255
|
+
options += " --generate-hashes"
|
256
|
+
end
|
257
|
+
|
258
|
+
if includes_unsafe_packages?(requirements_file.content)
|
259
|
+
options += " --allow-unsafe"
|
260
|
+
end
|
261
|
+
|
262
|
+
unless requirements_file.content.include?("# via ")
|
263
|
+
options += " --no-annotate"
|
264
|
+
end
|
265
|
+
|
266
|
+
unless requirements_file.content.include?("autogenerated by pip-c")
|
267
|
+
options += " --no-header"
|
268
|
+
end
|
269
|
+
|
270
|
+
options.strip
|
271
|
+
end
|
272
|
+
|
273
|
+
def includes_unsafe_packages?(content)
|
274
|
+
UNSAFE_PACKAGES.any? { |n| content.match?(/^#{Regexp.quote(n)}==/) }
|
275
|
+
end
|
276
|
+
|
277
|
+
def filenames_to_compile
|
278
|
+
files_from_reqs =
|
279
|
+
dependency.requirements.
|
280
|
+
map { |r| r[:file] }.
|
281
|
+
select { |fn| fn.end_with?(".in") }
|
282
|
+
|
283
|
+
files_from_compiled_files =
|
284
|
+
pip_compile_files.map(&:name).select do |fn|
|
285
|
+
compiled_file = dependency_files.
|
286
|
+
find { |f| f.name == fn.gsub(/\.in$/, ".txt") }
|
287
|
+
compiled_file_includes_dependency?(compiled_file)
|
288
|
+
end
|
289
|
+
|
290
|
+
filenames = [*files_from_reqs, *files_from_compiled_files].uniq
|
291
|
+
|
292
|
+
order_filenames_for_compilation(filenames)
|
293
|
+
end
|
294
|
+
|
295
|
+
def compiled_file_includes_dependency?(compiled_file)
|
296
|
+
return false unless compiled_file
|
297
|
+
|
298
|
+
regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
|
299
|
+
|
300
|
+
matches = []
|
301
|
+
compiled_file.content.scan(regex) { matches << Regexp.last_match }
|
302
|
+
matches.any? { |m| normalise(m[:name]) == dependency.name }
|
303
|
+
end
|
304
|
+
|
305
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
306
|
+
def normalise(name)
|
307
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
308
|
+
end
|
309
|
+
|
310
|
+
# If the files we need to update require one another then we need to
|
311
|
+
# update them in the right order
|
312
|
+
def order_filenames_for_compilation(filenames)
|
313
|
+
ordered_filenames = []
|
314
|
+
|
315
|
+
while (remaining_filenames = filenames - ordered_filenames).any?
|
316
|
+
ordered_filenames +=
|
317
|
+
remaining_filenames.
|
318
|
+
select do |fn|
|
319
|
+
unupdated_reqs = requirement_map[fn] - ordered_filenames
|
320
|
+
(unupdated_reqs & filenames).empty?
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
ordered_filenames
|
325
|
+
end
|
326
|
+
|
327
|
+
def requirement_map
|
328
|
+
child_req_regex = Python::FileFetcher::CHILD_REQUIREMENT_REGEX
|
329
|
+
@requirement_map ||=
|
330
|
+
pip_compile_files.each_with_object({}) do |file, req_map|
|
331
|
+
paths = file.content.scan(child_req_regex).flatten
|
332
|
+
current_dir = File.dirname(file.name)
|
333
|
+
|
334
|
+
req_map[file.name] =
|
335
|
+
paths.map do |path|
|
336
|
+
path = File.join(current_dir, path) if current_dir != "."
|
337
|
+
path = Pathname.new(path).cleanpath.to_path
|
338
|
+
path = path.gsub(/\.txt$/, ".in")
|
339
|
+
next if path == file.name
|
340
|
+
|
341
|
+
path
|
342
|
+
end.uniq.compact
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def setup_files
|
347
|
+
dependency_files.select { |f| f.name.end_with?("setup.py") }
|
348
|
+
end
|
349
|
+
|
350
|
+
def pip_compile_files
|
351
|
+
dependency_files.select { |f| f.name.end_with?(".in") }
|
352
|
+
end
|
353
|
+
|
354
|
+
def setup_cfg_files
|
355
|
+
dependency_files.select { |f| f.name.end_with?("setup.cfg") }
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
# rubocop:enable Metrics/ClassLength
|