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,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/python/requirement_parser"
|
4
|
+
require "dependabot/python/file_updater"
|
5
|
+
require "dependabot/shared_helpers"
|
6
|
+
|
7
|
+
module Dependabot
|
8
|
+
module Python
|
9
|
+
class FileUpdater
|
10
|
+
class RequirementReplacer
|
11
|
+
attr_reader :content, :dependency_name, :old_requirement,
|
12
|
+
:new_requirement
|
13
|
+
|
14
|
+
def initialize(content:, dependency_name:, old_requirement:,
|
15
|
+
new_requirement:)
|
16
|
+
@content = content
|
17
|
+
@dependency_name = dependency_name
|
18
|
+
@old_requirement = old_requirement
|
19
|
+
@new_requirement = new_requirement
|
20
|
+
end
|
21
|
+
|
22
|
+
def updated_content
|
23
|
+
updated_content =
|
24
|
+
content.gsub(original_declaration_replacement_regex) do |mtch|
|
25
|
+
# If the "declaration" is setting an option (e.g., no-binary)
|
26
|
+
# ignore it, since it isn't actually a declaration
|
27
|
+
next mtch if Regexp.last_match.pre_match.match?(/--.*\z/)
|
28
|
+
|
29
|
+
updated_dependency_declaration_string(
|
30
|
+
old_requirement,
|
31
|
+
new_requirement
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
raise "Expected content to change!" if content == updated_content
|
36
|
+
|
37
|
+
updated_content
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def original_dependency_declaration_string(old_req)
|
43
|
+
matches = []
|
44
|
+
|
45
|
+
dec =
|
46
|
+
if old_req.nil?
|
47
|
+
regex = RequirementParser::INSTALL_REQ_WITHOUT_REQUIREMENT
|
48
|
+
content.scan(regex) { matches << Regexp.last_match }
|
49
|
+
matches.find { |m| normalise(m[:name]) == dependency_name }
|
50
|
+
else
|
51
|
+
regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
|
52
|
+
content.scan(regex) { matches << Regexp.last_match }
|
53
|
+
matches.
|
54
|
+
select { |m| normalise(m[:name]) == dependency_name }.
|
55
|
+
find { |m| requirements_match(m[:requirements], old_req) }
|
56
|
+
end
|
57
|
+
|
58
|
+
raise "Declaration not found for #{dependency_name}!" unless dec
|
59
|
+
|
60
|
+
dec.to_s.strip
|
61
|
+
end
|
62
|
+
|
63
|
+
def updated_dependency_declaration_string(old_req, new_req)
|
64
|
+
if old_req
|
65
|
+
original_dependency_declaration_string(old_req).
|
66
|
+
sub(RequirementParser::REQUIREMENTS, new_req)
|
67
|
+
else
|
68
|
+
original_dependency_declaration_string(old_req).
|
69
|
+
sub(RequirementParser::NAME_WITH_EXTRAS) do |nm|
|
70
|
+
nm + new_req
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def original_declaration_replacement_regex
|
76
|
+
original_string =
|
77
|
+
original_dependency_declaration_string(old_requirement)
|
78
|
+
/(?<![\-\w\.])#{Regexp.escape(original_string)}(?![\-\w\.])/
|
79
|
+
end
|
80
|
+
|
81
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
82
|
+
def normalise(name)
|
83
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
84
|
+
end
|
85
|
+
|
86
|
+
def requirements_match(req1, req2)
|
87
|
+
req1&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort ==
|
88
|
+
req2&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/python/file_updater"
|
4
|
+
require "dependabot/python/file_parser/setup_file_parser"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Python
|
8
|
+
class FileUpdater
|
9
|
+
# Take a setup.py, parses it (carefully!) and then create a new, clean
|
10
|
+
# setup.py using only the information which will appear in the lockfile.
|
11
|
+
class SetupFileSanitizer
|
12
|
+
def initialize(setup_file:, setup_cfg:)
|
13
|
+
@setup_file = setup_file
|
14
|
+
@setup_cfg = setup_cfg
|
15
|
+
end
|
16
|
+
|
17
|
+
def sanitized_content
|
18
|
+
# The part of the setup.py that Pipenv cares about appears to be the
|
19
|
+
# install_requires. A name and version are required by don't end up
|
20
|
+
# in the lockfile.
|
21
|
+
content =
|
22
|
+
"from setuptools import setup\n\n"\
|
23
|
+
"setup(name=\"sanitized-package\",version=\"0.0.1\","\
|
24
|
+
"install_requires=#{install_requires_array.to_json},"\
|
25
|
+
"extras_require=#{extras_require_hash.to_json}"
|
26
|
+
|
27
|
+
content += ',setup_requires=["pbr"],pbr=True' if include_pbr?
|
28
|
+
content + ")"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :setup_file, :setup_cfg
|
34
|
+
|
35
|
+
def include_pbr?
|
36
|
+
setup_requires_array.any? { |d| d.start_with?("pbr") }
|
37
|
+
end
|
38
|
+
|
39
|
+
def install_requires_array
|
40
|
+
@install_requires_array ||=
|
41
|
+
parsed_setup_file.dependencies.map do |dep|
|
42
|
+
next unless dep.requirements.first[:groups].
|
43
|
+
include?("install_requires")
|
44
|
+
|
45
|
+
dep.name + dep.requirements.first[:requirement].to_s
|
46
|
+
end.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup_requires_array
|
50
|
+
@setup_requires_array ||=
|
51
|
+
parsed_setup_file.dependencies.map do |dep|
|
52
|
+
next unless dep.requirements.first[:groups].
|
53
|
+
include?("setup_requires")
|
54
|
+
|
55
|
+
dep.name + dep.requirements.first[:requirement].to_s
|
56
|
+
end.compact
|
57
|
+
end
|
58
|
+
|
59
|
+
def extras_require_hash
|
60
|
+
@extras_require_hash ||=
|
61
|
+
begin
|
62
|
+
hash = {}
|
63
|
+
parsed_setup_file.dependencies.each do |dep|
|
64
|
+
dep.requirements.first[:groups].each do |group|
|
65
|
+
next unless group.start_with?("extras_require:")
|
66
|
+
|
67
|
+
hash[group.split(":").last] ||= []
|
68
|
+
hash[group.split(":").last] <<
|
69
|
+
dep.name + dep.requirements.first[:requirement].to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
hash
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def parsed_setup_file
|
78
|
+
@parsed_setup_file ||=
|
79
|
+
Python::FileParser::SetupFileParser.new(
|
80
|
+
dependency_files: [
|
81
|
+
setup_file&.dup&.tap { |f| f.name = "setup.py" },
|
82
|
+
setup_cfg&.dup&.tap { |f| f.name = "setup.cfg" }
|
83
|
+
].compact
|
84
|
+
).dependency_set
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "excon"
|
4
|
+
require "dependabot/metadata_finders"
|
5
|
+
require "dependabot/metadata_finders/base"
|
6
|
+
require "dependabot/shared_helpers"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Python
|
10
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
11
|
+
MAIN_PYPI_URL = "https://pypi.org/pypi"
|
12
|
+
|
13
|
+
def homepage_url
|
14
|
+
pypi_listing.dig("info", "home_page") || super
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def look_up_source
|
20
|
+
potential_source_urls = [
|
21
|
+
pypi_listing.dig("info", "home_page"),
|
22
|
+
pypi_listing.dig("info", "bugtrack_url"),
|
23
|
+
pypi_listing.dig("info", "download_url"),
|
24
|
+
pypi_listing.dig("info", "docs_url")
|
25
|
+
].compact
|
26
|
+
|
27
|
+
source_url = potential_source_urls.find { |url| Source.from_url(url) }
|
28
|
+
source_url ||= source_from_description
|
29
|
+
source_url ||= source_from_homepage
|
30
|
+
|
31
|
+
Source.from_url(source_url)
|
32
|
+
end
|
33
|
+
|
34
|
+
def source_from_description
|
35
|
+
github_urls = []
|
36
|
+
desc = pypi_listing.dig("info", "description")
|
37
|
+
return unless desc
|
38
|
+
|
39
|
+
desc.scan(Source::SOURCE_REGEX) do
|
40
|
+
github_urls << Regexp.last_match.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
github_urls.find do |url|
|
44
|
+
repo = Source.from_url(url).repo
|
45
|
+
repo.downcase.end_with?(dependency.name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def source_from_homepage
|
50
|
+
return unless homepage_body
|
51
|
+
|
52
|
+
github_urls = []
|
53
|
+
homepage_body.scan(Source::SOURCE_REGEX) do
|
54
|
+
github_urls << Regexp.last_match.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
github_urls.find do |url|
|
58
|
+
repo = Source.from_url(url).repo
|
59
|
+
repo.downcase.end_with?(dependency.name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def homepage_body
|
64
|
+
homepage_url = pypi_listing.dig("info", "home_page")
|
65
|
+
|
66
|
+
return unless homepage_url
|
67
|
+
return if homepage_url.include?("pypi.python.org")
|
68
|
+
return if homepage_url.include?("pypi.org")
|
69
|
+
|
70
|
+
@homepage_response ||=
|
71
|
+
begin
|
72
|
+
Excon.get(
|
73
|
+
homepage_url,
|
74
|
+
idempotent: true,
|
75
|
+
**SharedHelpers.excon_defaults
|
76
|
+
)
|
77
|
+
rescue Excon::Error::Timeout, Excon::Error::Socket, ArgumentError
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
return unless @homepage_response&.status == 200
|
82
|
+
|
83
|
+
@homepage_response.body
|
84
|
+
end
|
85
|
+
|
86
|
+
def pypi_listing
|
87
|
+
return @pypi_listing unless @pypi_listing.nil?
|
88
|
+
return @pypi_listing = {} if dependency.version.include?("+")
|
89
|
+
|
90
|
+
possible_listing_urls.each do |url|
|
91
|
+
response = Excon.get(
|
92
|
+
url,
|
93
|
+
idempotent: true,
|
94
|
+
**SharedHelpers.excon_defaults
|
95
|
+
)
|
96
|
+
next unless response.status == 200
|
97
|
+
|
98
|
+
@pypi_listing = JSON.parse(response.body)
|
99
|
+
return @pypi_listing
|
100
|
+
rescue JSON::ParserError
|
101
|
+
next
|
102
|
+
end
|
103
|
+
|
104
|
+
@pypi_listing = {} # No listing found
|
105
|
+
end
|
106
|
+
|
107
|
+
def possible_listing_urls
|
108
|
+
credential_urls =
|
109
|
+
credentials.
|
110
|
+
select { |cred| cred["type"] == "python_index" }.
|
111
|
+
map { |cred| cred["index-url"].gsub(%r{/$}, "") }
|
112
|
+
|
113
|
+
(credential_urls + [MAIN_PYPI_URL]).map do |base_url|
|
114
|
+
base_url + "/#{dependency.name}/json"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Dependabot::MetadataFinders.
|
122
|
+
register("pip", Dependabot::Python::MetadataFinder)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dependabot
|
4
|
+
module Python
|
5
|
+
module NativeHelpers
|
6
|
+
def self.python_helper_path
|
7
|
+
helpers_dir = File.join(native_helpers_root, "python/helpers")
|
8
|
+
Pathname.new(File.join(helpers_dir, "run.py")).cleanpath.to_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.native_helpers_root
|
12
|
+
default_path = File.join(__dir__, "../../../..")
|
13
|
+
ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dependabot
|
4
|
+
module Python
|
5
|
+
module PythonVersions
|
6
|
+
# Poetry doesn't handle Python versions, so we have to do so manually
|
7
|
+
# (checking from a list of versions Poetry supports).
|
8
|
+
# This list gets iterated through to find a valid version, so we have
|
9
|
+
# the two pre-installed versions listed first.
|
10
|
+
PYTHON_VERSIONS = %w(
|
11
|
+
3.6.7 2.7.15
|
12
|
+
3.7.1 3.7.0
|
13
|
+
3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.6.0
|
14
|
+
3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0
|
15
|
+
3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0
|
16
|
+
2.7.15 2.7.14 2.7.13 2.7.12 2.7.11 2.7.10 2.7.9 2.7.8 2.7.7 2.7.6 2.7.5
|
17
|
+
2.7.4 2.7.3 2.7.2 2.7.1 2.7
|
18
|
+
).freeze
|
19
|
+
|
20
|
+
PRE_INSTALLED_PYTHON_VERSIONS = %w(
|
21
|
+
3.6.7 2.7.15
|
22
|
+
).freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/python/version"
|
4
|
+
|
5
|
+
module Dependabot
|
6
|
+
module Python
|
7
|
+
class Requirement < Gem::Requirement
|
8
|
+
OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/.freeze
|
9
|
+
|
10
|
+
# Add equality and arbitrary-equality matchers
|
11
|
+
OPS["=="] = ->(v, r) { v == r }
|
12
|
+
OPS["==="] = ->(v, r) { v.to_s == r.to_s }
|
13
|
+
|
14
|
+
quoted = OPS.keys.sort_by(&:length).reverse.
|
15
|
+
map { |k| Regexp.quote(k) }.join("|")
|
16
|
+
version_pattern = Python::Version::VERSION_PATTERN
|
17
|
+
|
18
|
+
PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
|
19
|
+
PATTERN = /\A#{PATTERN_RAW}\z/.freeze
|
20
|
+
|
21
|
+
def self.parse(obj)
|
22
|
+
return ["=", Python::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
|
23
|
+
|
24
|
+
unless (matches = PATTERN.match(obj.to_s))
|
25
|
+
msg = "Illformed requirement [#{obj.inspect}]"
|
26
|
+
raise BadRequirementError, msg
|
27
|
+
end
|
28
|
+
|
29
|
+
return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
|
30
|
+
|
31
|
+
[matches[1] || "=", Python::Version.new(matches[2])]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns an array of requirements. At least one requirement from the
|
35
|
+
# returned array must be satisfied for a version to be valid.
|
36
|
+
#
|
37
|
+
# NOTE: Or requirements are only valid for Poetry.
|
38
|
+
def self.requirements_array(requirement_string)
|
39
|
+
return [new(nil)] if requirement_string.nil?
|
40
|
+
|
41
|
+
requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
|
42
|
+
new(req_string.strip)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(*requirements)
|
47
|
+
requirements = requirements.flatten.flat_map do |req_string|
|
48
|
+
next if req_string.nil?
|
49
|
+
|
50
|
+
req_string.split(",").map do |r|
|
51
|
+
convert_python_constraint_to_ruby_constraint(r)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
super(requirements)
|
56
|
+
end
|
57
|
+
|
58
|
+
def satisfied_by?(version)
|
59
|
+
version = Python::Version.new(version.to_s)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def exact?
|
64
|
+
return false unless @requirements.size == 1
|
65
|
+
|
66
|
+
%w(= == ===).include?(@requirements[0][0])
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def convert_python_constraint_to_ruby_constraint(req_string)
|
72
|
+
return nil if req_string.nil?
|
73
|
+
return nil if req_string == "*"
|
74
|
+
|
75
|
+
req_string = req_string.gsub("~=", "~>")
|
76
|
+
req_string = req_string.gsub(/(?<=\d)[<=>].*/, "")
|
77
|
+
|
78
|
+
if req_string.match?(/~[^>]/) then convert_tilde_req(req_string)
|
79
|
+
elsif req_string.start_with?("^") then convert_caret_req(req_string)
|
80
|
+
elsif req_string.include?(".*") then convert_wildcard(req_string)
|
81
|
+
else req_string
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Poetry uses ~ requirements.
|
86
|
+
# https://github.com/sdispater/poetry#tilde-requirements
|
87
|
+
def convert_tilde_req(req_string)
|
88
|
+
version = req_string.gsub(/^~\>?/, "")
|
89
|
+
parts = version.split(".")
|
90
|
+
parts << "0" if parts.count < 3
|
91
|
+
"~> #{parts.join('.')}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Poetry uses ^ requirements
|
95
|
+
# https://github.com/sdispater/poetry#caret-requirement
|
96
|
+
def convert_caret_req(req_string)
|
97
|
+
version = req_string.gsub(/^\^/, "")
|
98
|
+
parts = version.split(".")
|
99
|
+
parts = parts.fill(0, parts.length...3)
|
100
|
+
first_non_zero = parts.find { |d| d != "0" }
|
101
|
+
first_non_zero_index =
|
102
|
+
first_non_zero ? parts.index(first_non_zero) : parts.count - 1
|
103
|
+
upper_bound = parts.map.with_index do |part, i|
|
104
|
+
if i < first_non_zero_index then part
|
105
|
+
elsif i == first_non_zero_index then (part.to_i + 1).to_s
|
106
|
+
elsif i > first_non_zero_index && i == 2 then "0.a"
|
107
|
+
else 0
|
108
|
+
end
|
109
|
+
end.join(".")
|
110
|
+
|
111
|
+
[">= #{version}", "< #{upper_bound}"]
|
112
|
+
end
|
113
|
+
|
114
|
+
def convert_wildcard(req_string)
|
115
|
+
# Note: This isn't perfect. It replaces the "!= 1.0.*" case with
|
116
|
+
# "!= 1.0.0". There's no way to model this correctly in Ruby :'(
|
117
|
+
req_string.
|
118
|
+
split(".").
|
119
|
+
first(req_string.split(".").index("*") + 1).
|
120
|
+
join(".").
|
121
|
+
tr("*", "0").
|
122
|
+
gsub(/^(?<!!)=*/, "~>")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
Dependabot::Utils.
|
129
|
+
register_requirement_class("pip", Dependabot::Python::Requirement)
|