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