dependabot-python 0.79.0

Sign up to get free protection for your applications and to get access to all the features.
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