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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +17 -0
  3. data/helpers/lib/__init__.py +0 -0
  4. data/helpers/lib/hasher.py +23 -0
  5. data/helpers/lib/parser.py +130 -0
  6. data/helpers/requirements.txt +9 -0
  7. data/helpers/run.py +18 -0
  8. data/lib/dependabot/python.rb +11 -0
  9. data/lib/dependabot/python/file_fetcher.rb +307 -0
  10. data/lib/dependabot/python/file_parser.rb +221 -0
  11. data/lib/dependabot/python/file_parser/pipfile_files_parser.rb +150 -0
  12. data/lib/dependabot/python/file_parser/poetry_files_parser.rb +139 -0
  13. data/lib/dependabot/python/file_parser/setup_file_parser.rb +158 -0
  14. data/lib/dependabot/python/file_updater.rb +149 -0
  15. data/lib/dependabot/python/file_updater/pip_compile_file_updater.rb +361 -0
  16. data/lib/dependabot/python/file_updater/pipfile_file_updater.rb +391 -0
  17. data/lib/dependabot/python/file_updater/pipfile_preparer.rb +123 -0
  18. data/lib/dependabot/python/file_updater/poetry_file_updater.rb +282 -0
  19. data/lib/dependabot/python/file_updater/pyproject_preparer.rb +103 -0
  20. data/lib/dependabot/python/file_updater/requirement_file_updater.rb +160 -0
  21. data/lib/dependabot/python/file_updater/requirement_replacer.rb +93 -0
  22. data/lib/dependabot/python/file_updater/setup_file_sanitizer.rb +89 -0
  23. data/lib/dependabot/python/metadata_finder.rb +122 -0
  24. data/lib/dependabot/python/native_helpers.rb +17 -0
  25. data/lib/dependabot/python/python_versions.rb +25 -0
  26. data/lib/dependabot/python/requirement.rb +129 -0
  27. data/lib/dependabot/python/requirement_parser.rb +38 -0
  28. data/lib/dependabot/python/update_checker.rb +229 -0
  29. data/lib/dependabot/python/update_checker/latest_version_finder.rb +250 -0
  30. data/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +379 -0
  31. data/lib/dependabot/python/update_checker/pipfile_version_resolver.rb +558 -0
  32. data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +298 -0
  33. data/lib/dependabot/python/update_checker/requirements_updater.rb +365 -0
  34. data/lib/dependabot/python/version.rb +87 -0
  35. 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