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