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