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,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers"
7
+ require "dependabot/file_parsers/base"
8
+ require "dependabot/file_parsers/base/dependency_set"
9
+ require "dependabot/shared_helpers"
10
+ require "dependabot/python/requirement"
11
+ require "dependabot/errors"
12
+ require "dependabot/python/native_helpers"
13
+
14
+ module Dependabot
15
+ module Python
16
+ class FileParser < Dependabot::FileParsers::Base
17
+ require_relative "file_parser/pipfile_files_parser"
18
+ require_relative "file_parser/poetry_files_parser"
19
+ require_relative "file_parser/setup_file_parser"
20
+
21
+ POETRY_DEPENDENCY_TYPES =
22
+ %w(tool.poetry.dependencies tool.poetry.dev-dependencies).freeze
23
+ DEPENDENCY_GROUP_KEYS = [
24
+ {
25
+ pipfile: "packages",
26
+ lockfile: "default"
27
+ },
28
+ {
29
+ pipfile: "dev-packages",
30
+ lockfile: "develop"
31
+ }
32
+ ].freeze
33
+ REQUIREMENT_FILE_EVALUATION_ERRORS = %w(
34
+ InstallationError RequirementsFileParseError InvalidMarker
35
+ InvalidRequirement
36
+ ).freeze
37
+
38
+ def parse
39
+ dependency_set = DependencySet.new
40
+
41
+ dependency_set += pipenv_dependencies if pipfile
42
+ dependency_set += poetry_dependencies if using_poetry?
43
+ dependency_set += requirement_dependencies if requirement_files.any?
44
+ dependency_set += setup_file_dependencies if setup_file
45
+
46
+ dependency_set.dependencies
47
+ end
48
+
49
+ private
50
+
51
+ def requirement_files
52
+ dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
53
+ end
54
+
55
+ def pipenv_dependencies
56
+ @pipenv_dependencies ||=
57
+ PipfileFilesParser.
58
+ new(dependency_files: dependency_files).
59
+ dependency_set
60
+ end
61
+
62
+ def poetry_dependencies
63
+ @poetry_dependencies ||=
64
+ PoetryFilesParser.
65
+ new(dependency_files: dependency_files).
66
+ dependency_set
67
+ end
68
+
69
+ def requirement_dependencies
70
+ dependencies = DependencySet.new
71
+ parsed_requirement_files.each do |dep|
72
+ # This isn't ideal, but currently the FileUpdater won't update
73
+ # deps that appear in a requirements.txt and Pipfile / Pipfile.lock
74
+ # and *aren't* a straight lockfile for the Pipfile
75
+ next if included_in_pipenv_deps?(normalised_name(dep["name"]))
76
+
77
+ # If a requirement has a `<` or `<=` marker then updating it is
78
+ # probably blocked. Ignore it.
79
+ next if dep["markers"].include?("<")
80
+
81
+ requirements =
82
+ if lockfile_for_pip_compile_file?(dep["file"]) then []
83
+ else
84
+ [{
85
+ requirement: dep["requirement"],
86
+ file: Pathname.new(dep["file"]).cleanpath.to_path,
87
+ source: nil,
88
+ groups: []
89
+ }]
90
+ end
91
+
92
+ dependencies <<
93
+ Dependency.new(
94
+ name: normalised_name(dep["name"]),
95
+ version: dep["version"]&.include?("*") ? nil : dep["version"],
96
+ requirements: requirements,
97
+ package_manager: "pip"
98
+ )
99
+ end
100
+ dependencies
101
+ end
102
+
103
+ def included_in_pipenv_deps?(dep_name)
104
+ return false unless pipfile
105
+
106
+ pipenv_dependencies.dependencies.map(&:name).include?(dep_name)
107
+ end
108
+
109
+ def setup_file_dependencies
110
+ @setup_file_dependencies ||=
111
+ SetupFileParser.
112
+ new(dependency_files: dependency_files).
113
+ dependency_set
114
+ end
115
+
116
+ def lockfile_for_pip_compile_file?(filename)
117
+ return false unless pip_compile_files.any?
118
+ return false unless filename.end_with?(".txt")
119
+
120
+ basename = filename.gsub(/\.txt$/, "")
121
+ pip_compile_files.any? { |f| f.name == basename + ".in" }
122
+ end
123
+
124
+ def parsed_requirement_files
125
+ SharedHelpers.in_a_temporary_directory do
126
+ write_temporary_dependency_files
127
+
128
+ requirements = SharedHelpers.run_helper_subprocess(
129
+ command: "pyenv exec python #{NativeHelpers.python_helper_path}",
130
+ function: "parse_requirements",
131
+ args: [Dir.pwd]
132
+ )
133
+
134
+ check_requirements(requirements)
135
+ requirements
136
+ end
137
+ rescue SharedHelpers::HelperSubprocessFailed => error
138
+ evaluation_errors = REQUIREMENT_FILE_EVALUATION_ERRORS
139
+ raise unless error.message.start_with?(*evaluation_errors)
140
+
141
+ raise Dependabot::DependencyFileNotEvaluatable, error.message
142
+ end
143
+
144
+ def check_requirements(requirements)
145
+ requirements.each do |dep|
146
+ next unless dep["requirement"]
147
+
148
+ Python::Requirement.new(dep["requirement"].split(","))
149
+ rescue Gem::Requirement::BadRequirementError => error
150
+ raise Dependabot::DependencyFileNotEvaluatable, error.message
151
+ end
152
+ end
153
+
154
+ def write_temporary_dependency_files
155
+ dependency_files.
156
+ reject { |f| f.name == ".python-version" }.
157
+ each do |file|
158
+ path = file.name
159
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
160
+ File.write(path, file.content)
161
+ end
162
+ end
163
+
164
+ # See https://www.python.org/dev/peps/pep-0503/#normalized-names
165
+ def normalised_name(name)
166
+ name.downcase.gsub(/[-_.]+/, "-")
167
+ end
168
+
169
+ def check_required_files
170
+ filenames = dependency_files.map(&:name)
171
+ return if filenames.any? { |name| name.end_with?(".txt", ".in") }
172
+ return if pipfile
173
+ return if pyproject
174
+ return if setup_file
175
+
176
+ raise "No requirements.txt or setup.py!"
177
+ end
178
+
179
+ def pipfile
180
+ @pipfile ||= get_original_file("Pipfile")
181
+ end
182
+
183
+ def pipfile_lock
184
+ @pipfile_lock ||= get_original_file("Pipfile.lock")
185
+ end
186
+
187
+ def using_poetry?
188
+ return false unless pyproject
189
+ return true if poetry_lock || pyproject_lock
190
+
191
+ !TomlRB.parse(pyproject.content).dig("tool", "poetry").nil?
192
+ rescue TomlRB::ParseError
193
+ raise Dependabot::DependencyFileNotParseable, pyproject.path
194
+ end
195
+
196
+ def pyproject
197
+ @pyproject ||= get_original_file("pyproject.toml")
198
+ end
199
+
200
+ def pyproject_lock
201
+ @pyproject_lock ||= get_original_file("pyproject.lock")
202
+ end
203
+
204
+ def poetry_lock
205
+ @poetry_lock ||= get_original_file("poetry.lock")
206
+ end
207
+
208
+ def setup_file
209
+ @setup_file ||= get_original_file("setup.py")
210
+ end
211
+
212
+ def pip_compile_files
213
+ @pip_compile_files ||=
214
+ dependency_files.select { |f| f.name.end_with?(".in") }
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ Dependabot::FileParsers.
221
+ register("pip", Dependabot::Python::FileParser)
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers/base/dependency_set"
7
+ require "dependabot/python/file_parser"
8
+ require "dependabot/errors"
9
+
10
+ module Dependabot
11
+ module Python
12
+ class FileParser
13
+ class PipfileFilesParser
14
+ DEPENDENCY_GROUP_KEYS = [
15
+ {
16
+ pipfile: "packages",
17
+ lockfile: "default"
18
+ },
19
+ {
20
+ pipfile: "dev-packages",
21
+ lockfile: "develop"
22
+ }
23
+ ].freeze
24
+
25
+ def initialize(dependency_files:)
26
+ @dependency_files = dependency_files
27
+ end
28
+
29
+ def dependency_set
30
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
31
+
32
+ dependency_set += pipfile_dependencies
33
+ dependency_set += pipfile_lock_dependencies
34
+
35
+ dependency_set
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :dependency_files
41
+
42
+ def pipfile_dependencies
43
+ dependencies = Dependabot::FileParsers::Base::DependencySet.new
44
+
45
+ DEPENDENCY_GROUP_KEYS.each do |keys|
46
+ next unless parsed_pipfile[keys[:pipfile]]
47
+
48
+ parsed_pipfile[keys[:pipfile]].map do |dep_name, req|
49
+ group = keys[:lockfile]
50
+ next unless req.is_a?(String) || req["version"]
51
+ next if pipfile_lock && !dependency_version(dep_name, req, group)
52
+
53
+ dependencies <<
54
+ Dependency.new(
55
+ name: normalised_name(dep_name),
56
+ version: dependency_version(dep_name, req, group),
57
+ requirements: [{
58
+ requirement: req.is_a?(String) ? req : req["version"],
59
+ file: pipfile.name,
60
+ source: nil,
61
+ groups: [group]
62
+ }],
63
+ package_manager: "pip"
64
+ )
65
+ end
66
+ end
67
+
68
+ dependencies
69
+ end
70
+
71
+ # Create a DependencySet where each element has no requirement. Any
72
+ # requirements will be added when combining the DependencySet with
73
+ # other DependencySets.
74
+ def pipfile_lock_dependencies
75
+ dependencies = Dependabot::FileParsers::Base::DependencySet.new
76
+ return dependencies unless pipfile_lock
77
+
78
+ DEPENDENCY_GROUP_KEYS.map { |h| h.fetch(:lockfile) }.each do |key|
79
+ next unless parsed_pipfile_lock[key]
80
+
81
+ parsed_pipfile_lock[key].each do |dep_name, details|
82
+ version = case details
83
+ when String then details
84
+ when Hash then details["version"]
85
+ end
86
+ next unless version
87
+
88
+ dependencies <<
89
+ Dependency.new(
90
+ name: dep_name,
91
+ version: version&.gsub(/^===?/, ""),
92
+ requirements: [],
93
+ package_manager: "pip"
94
+ )
95
+ end
96
+ end
97
+
98
+ dependencies
99
+ end
100
+
101
+ def dependency_version(dep_name, requirement, group)
102
+ req = version_from_hash_or_string(requirement)
103
+
104
+ if pipfile_lock
105
+ details = parsed_pipfile_lock.
106
+ dig(group, normalised_name(dep_name))
107
+
108
+ version = version_from_hash_or_string(details)
109
+ version&.gsub(/^===?/, "")
110
+ elsif req.start_with?("==") && !req.include?("*")
111
+ req.strip.gsub(/^===?/, "")
112
+ end
113
+ end
114
+
115
+ def version_from_hash_or_string(obj)
116
+ case obj
117
+ when String then obj.strip
118
+ when Hash then obj["version"]
119
+ end
120
+ end
121
+
122
+ # See https://www.python.org/dev/peps/pep-0503/#normalized-names
123
+ def normalised_name(name)
124
+ name.downcase.gsub(/[-_.]+/, "-")
125
+ end
126
+
127
+ def parsed_pipfile
128
+ @parsed_pipfile ||= TomlRB.parse(pipfile.content)
129
+ rescue TomlRB::ParseError
130
+ raise Dependabot::DependencyFileNotParseable, pipfile.path
131
+ end
132
+
133
+ def parsed_pipfile_lock
134
+ @parsed_pipfile_lock ||= JSON.parse(pipfile_lock.content)
135
+ rescue JSON::ParserError
136
+ raise Dependabot::DependencyFileNotParseable, pipfile_lock.path
137
+ end
138
+
139
+ def pipfile
140
+ @pipfile ||= dependency_files.find { |f| f.name == "Pipfile" }
141
+ end
142
+
143
+ def pipfile_lock
144
+ @pipfile_lock ||=
145
+ dependency_files.find { |f| f.name == "Pipfile.lock" }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers/base/dependency_set"
7
+ require "dependabot/python/file_parser"
8
+ require "dependabot/errors"
9
+
10
+ module Dependabot
11
+ module Python
12
+ class FileParser
13
+ class PoetryFilesParser
14
+ POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze
15
+
16
+ def initialize(dependency_files:)
17
+ @dependency_files = dependency_files
18
+ end
19
+
20
+ def dependency_set
21
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
22
+
23
+ dependency_set += pyproject_dependencies
24
+ dependency_set += lockfile_dependencies if lockfile
25
+
26
+ dependency_set
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :dependency_files
32
+
33
+ def pyproject_dependencies
34
+ dependencies = Dependabot::FileParsers::Base::DependencySet.new
35
+
36
+ POETRY_DEPENDENCY_TYPES.each do |type|
37
+ deps_hash = parsed_pyproject.dig("tool", "poetry", type) || {}
38
+
39
+ deps_hash.each do |name, req|
40
+ next if normalised_name(name) == "python"
41
+ next if req.is_a?(Hash) && req.key?("git")
42
+
43
+ dependencies <<
44
+ Dependency.new(
45
+ name: normalised_name(name),
46
+ version: version_from_lockfile(name),
47
+ requirements: [{
48
+ requirement: req.is_a?(String) ? req : req["version"],
49
+ file: pyproject.name,
50
+ source: nil,
51
+ groups: [type]
52
+ }],
53
+ package_manager: "pip"
54
+ )
55
+ end
56
+ end
57
+
58
+ dependencies
59
+ end
60
+
61
+ # Create a DependencySet where each element has no requirement. Any
62
+ # requirements will be added when combining the DependencySet with
63
+ # other DependencySets.
64
+ def lockfile_dependencies
65
+ dependencies = Dependabot::FileParsers::Base::DependencySet.new
66
+
67
+ parsed_lockfile.fetch("package", []).each do |details|
68
+ next if details.dig("source", "type") == "git"
69
+
70
+ dependencies <<
71
+ Dependency.new(
72
+ name: details.fetch("name"),
73
+ version: details.fetch("version"),
74
+ requirements: [],
75
+ package_manager: "pip"
76
+ )
77
+ end
78
+
79
+ dependencies
80
+ end
81
+
82
+ def version_from_lockfile(dep_name)
83
+ return unless parsed_lockfile
84
+
85
+ parsed_lockfile.fetch("package", []).
86
+ find { |p| p.fetch("name") == normalised_name(dep_name) }&.
87
+ fetch("verison", nil)
88
+ end
89
+
90
+ # See https://www.python.org/dev/peps/pep-0503/#normalized-names
91
+ def normalised_name(name)
92
+ name.downcase.gsub(/[-_.]+/, "-")
93
+ end
94
+
95
+ def parsed_pyproject
96
+ @parsed_pyproject ||= TomlRB.parse(pyproject.content)
97
+ rescue TomlRB::ParseError
98
+ raise Dependabot::DependencyFileNotParseable, pyproject.path
99
+ end
100
+
101
+ def parsed_pyproject_lock
102
+ @parsed_pyproject_lock ||= TomlRB.parse(pyproject_lock.content)
103
+ rescue TomlRB::ParseError
104
+ raise Dependabot::DependencyFileNotParseable, pyproject_lock.path
105
+ end
106
+
107
+ def parsed_poetry_lock
108
+ @parsed_poetry_lock ||= TomlRB.parse(poetry_lock.content)
109
+ rescue TomlRB::ParseError
110
+ raise Dependabot::DependencyFileNotParseable, poetry_lock.path
111
+ end
112
+
113
+ def pyproject
114
+ @pyproject ||=
115
+ dependency_files.find { |f| f.name == "pyproject.toml" }
116
+ end
117
+
118
+ def lockfile
119
+ poetry_lock || pyproject_lock
120
+ end
121
+
122
+ def parsed_lockfile
123
+ return parsed_poetry_lock if poetry_lock
124
+ return parsed_pyproject_lock if pyproject_lock
125
+ end
126
+
127
+ def pyproject_lock
128
+ @pyproject_lock ||=
129
+ dependency_files.find { |f| f.name == "pyproject.lock" }
130
+ end
131
+
132
+ def poetry_lock
133
+ @poetry_lock ||=
134
+ dependency_files.find { |f| f.name == "poetry.lock" }
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end