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,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