class Dependabot::Python::FileUpdater::PipfileFileUpdater
Attributes
credentials[R]
dependencies[R]
dependency_files[R]
Public Class Methods
new(dependencies:, dependency_files:, credentials:)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 23 def initialize(dependencies:, dependency_files:, credentials:) @dependencies = dependencies @dependency_files = dependency_files @credentials = credentials end
Public Instance Methods
updated_dependency_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 29 def updated_dependency_files return @updated_dependency_files if @update_already_attempted @update_already_attempted = true @updated_dependency_files ||= fetch_updated_dependency_files end
Private Instance Methods
add_private_sources(pipfile_content)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 179 def add_private_sources(pipfile_content) PipfilePreparer. new(pipfile_content: pipfile_content). replace_sources(credentials) end
dependency()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 38 def dependency # For now, we'll only ever be updating a single dependency dependencies.first end
fetch_updated_dependency_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 43 def fetch_updated_dependency_files updated_files = [] if pipfile.content != updated_pipfile_content updated_files << updated_file(file: pipfile, content: updated_pipfile_content) end if lockfile raise "Expected Pipfile.lock to change!" if lockfile.content == updated_lockfile_content updated_files << updated_file(file: lockfile, content: updated_lockfile_content) end updated_files += updated_generated_requirements_files updated_files end
freeze_dependencies_being_updated(pipfile_content)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 144 def freeze_dependencies_being_updated(pipfile_content) pipfile_object = TomlRB.parse(pipfile_content) dependencies.each do |dep| %w(packages dev-packages).each do |type| names = pipfile_object[type]&.keys || [] pkg_name = names.find { |nm| normalise(nm) == dep.name } next unless pkg_name || subdep_type?(type) pkg_name ||= dependency.name if pipfile_object[type][pkg_name].is_a?(Hash) pipfile_object[type][pkg_name]["version"] = "==#{dep.version}" else pipfile_object[type][pkg_name] = "==#{dep.version}" end end end TomlRB.dump(pipfile_object) end
freeze_other_dependencies(pipfile_content)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 137 def freeze_other_dependencies(pipfile_content) PipfilePreparer. new(pipfile_content: pipfile_content, lockfile: lockfile). freeze_top_level_dependencies_except(dependencies) end
generate_updated_requirements_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 232 def generate_updated_requirements_files req_content = run_pipenv_command( "pyenv exec pipenv lock -r" ) File.write("req.txt", req_content) dev_req_content = run_pipenv_command( "pyenv exec pipenv lock -r -d" ) File.write("dev-req.txt", dev_req_content) end
generate_updated_requirements_files?()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 75 def generate_updated_requirements_files? return true if generated_requirements_files("default").any? generated_requirements_files("develop").any? end
generated_requirements_files(type)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 81 def generated_requirements_files(type) return [] unless lockfile pipfile_lock_deps = parsed_lockfile[type]&.keys&.sort || [] pipfile_lock_deps = pipfile_lock_deps.map { |n| normalise(n) } return [] unless pipfile_lock_deps.any? regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT # Find any requirement files that list the same dependencies as # the (old) Pipfile.lock. Any such files were almost certainly # generated using `pipenv lock -r` requirements_files.select do |req_file| deps = [] req_file.content.scan(regex) { deps << Regexp.last_match } deps = deps.map { |m| normalise(m[:name]) } deps.sort == pipfile_lock_deps end end
install_required_python()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 295 def install_required_python # Initialize a git repo to appease pip-tools begin run_command("git init") if setup_files.any? rescue Dependabot::SharedHelpers::HelperSubprocessFailed nil end return if run_command("pyenv versions").include?("#{python_version}\n") requirements_path = NativeHelpers.python_requirements_path run_command("pyenv install -s #{python_version}") run_command("pyenv exec pip install -r #{requirements_path}") end
lockfile()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 405 def lockfile @lockfile ||= dependency_files.find { |f| f.name == "Pipfile.lock" } end
normalise(name)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 393 def normalise(name) NameNormaliser.normalise(name) end
parsed_lockfile()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 397 def parsed_lockfile @parsed_lockfile ||= JSON.parse(lockfile.content) end
pipenv_env_variables()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 421 def pipenv_env_variables { "PIPENV_YES" => "true", # Install new Python ver if needed "PIPENV_MAX_RETRIES" => "3", # Retry timeouts "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes "PIP_DEFAULT_TIMEOUT" => "60" # Set pip timeout to 1 minute } end
pipfile()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 401 def pipfile @pipfile ||= dependency_files.find { |f| f.name == "Pipfile" } end
pipfile_hash_for(pipfile_content)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 376 def pipfile_hash_for(pipfile_content) SharedHelpers.in_a_temporary_directory do |dir| File.write(File.join(dir, "Pipfile"), pipfile_content) SharedHelpers.run_helper_subprocess( command: "pyenv exec python #{NativeHelpers.python_helper_path}", function: "get_pipfile_hash", args: [dir] ) end end
post_process_lockfile(updated_lockfile_content)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 216 def post_process_lockfile(updated_lockfile_content) pipfile_hash = pipfile_hash_for(updated_pipfile_content) original_reqs = parsed_lockfile["_meta"]["requires"] original_source = parsed_lockfile["_meta"]["sources"] new_lockfile = updated_lockfile_content.dup new_lockfile_json = JSON.parse(new_lockfile) new_lockfile_json["_meta"]["hash"]["sha256"] = pipfile_hash new_lockfile_json["_meta"]["requires"] = original_reqs new_lockfile_json["_meta"]["sources"] = original_source JSON.pretty_generate(new_lockfile_json, indent: " "). gsub(/\{\n\s*\}/, "{}"). gsub(/\}\z/, "}\n") end
prepared_pipfile_content()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 129 def prepared_pipfile_content content = updated_pipfile_content content = freeze_other_dependencies(content) content = freeze_dependencies_being_updated(content) content = add_private_sources(content) content end
python_requirement_parser()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 363 def python_requirement_parser @python_requirement_parser ||= FileParser::PythonRequirementParser.new( dependency_files: dependency_files ) end
python_version()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 320 def python_version @python_version ||= python_version_from_supported_versions end
python_version_from_supported_versions()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 324 def python_version_from_supported_versions requirement_string = if @using_python_two then "2.7.*" elsif user_specified_python_requirement parts = user_specified_python_requirement.split(".") parts.fill("*", (parts.length)..2).join(".") else PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.first end # Ideally, the requirement is satisfied by a Python version we support requirement = Python::Requirement.requirements_array(requirement_string).first version = PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE. find { |v| requirement.satisfied_by?(Python::Version.new(v)) } return version if version # If not, and changing the patch version would fix things, we do that # as the patch version is unlikely to affect resolution requirement = Python::Requirement.new(requirement_string.gsub(/\.\d+$/, ".*")) version = PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE. find { |v| requirement.satisfied_by?(Python::Version.new(v)) } return version if version # Otherwise we have to raise, giving details of the Python versions # that Dependabot supports msg = "Dependabot detected the following Python requirement "\ "for your project: '#{requirement_string}'.\n\nCurrently, the "\ "following Python versions are supported in Dependabot: "\ "#{PythonVersions::SUPPORTED_VERSIONS.join(', ')}." raise DependencyFileNotResolvable, msg end
requirements_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 417 def requirements_files dependency_files.select { |f| f.name.end_with?(".txt") } end
run_command(command, env: {})
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 244 def run_command(command, env: {}) start = Time.now command = SharedHelpers.escape_command(command) stdout, process = Open3.capture2e(env, command) time_taken = Time.now - start # Raise an error with the output from the shell session if Pipenv # returns a non-zero status return stdout if process.success? raise SharedHelpers::HelperSubprocessFailed.new( message: stdout, error_context: { command: command, time_taken: time_taken, process_exit_value: process.to_s } ) end
run_pipenv_command(command, env: pipenv_env_variables)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 264 def run_pipenv_command(command, env: pipenv_env_variables) run_command("pyenv local #{python_version}") run_command(command, env: env) end
sanitized_setup_file_content(file)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 310 def sanitized_setup_file_content(file) @sanitized_setup_file_content ||= {} return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name] @sanitized_setup_file_content[file.name] = SetupFileSanitizer. new(setup_file: file, setup_cfg: setup_cfg(file)). sanitized_content end
setup_cfg(file)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 370 def setup_cfg(file) dependency_files.find do |f| f.name == file.name.sub(/\.py$/, ".cfg") end end
setup_cfg_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 413 def setup_cfg_files dependency_files.select { |f| f.name.end_with?("setup.cfg") } end
setup_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 409 def setup_files dependency_files.select { |f| f.name.end_with?("setup.py") } end
subdep_type?(type)
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 167 def subdep_type?(type) return false if dependency.top_level? lockfile_type = Python::FileParser::DEPENDENCY_GROUP_KEYS. find { |i| i.fetch(:pipfile) == type }. fetch(:lockfile) JSON.parse(lockfile.content). fetch(lockfile_type, {}). keys.any? { |k| normalise(k) == dependency.name } end
updated_dev_req_content()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 125 def updated_dev_req_content updated_generated_files.fetch(:dev_requirements_txt) end
updated_file(file:, content:)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 387 def updated_file(file:, content:) updated_file = file.dup updated_file.content = content updated_file end
updated_generated_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 185 def updated_generated_files @updated_generated_files ||= SharedHelpers.in_a_temporary_directory do SharedHelpers.with_git_configured(credentials: credentials) do write_temporary_dependency_files(prepared_pipfile_content) install_required_python # Initialize a git repo to appease pip-tools command = SharedHelpers.escape_command("git init") IO.popen(command, err: %i(child out)) if setup_files.any? run_pipenv_command( "pyenv exec pipenv lock" ) result = { lockfile: File.read("Pipfile.lock") } result[:lockfile] = post_process_lockfile(result[:lockfile]) # Generate updated requirement.txt entries, if needed. if generate_updated_requirements_files? generate_updated_requirements_files result[:requirements_txt] = File.read("req.txt") result[:dev_requirements_txt] = File.read("dev-req.txt") end result end end end
updated_generated_requirements_files()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 101 def updated_generated_requirements_files updated_files = [] generated_requirements_files("default").each do |file| next if file.content == updated_req_content updated_files << updated_file(file: file, content: updated_req_content) end generated_requirements_files("develop").each do |file| next if file.content == updated_dev_req_content updated_files << updated_file(file: file, content: updated_dev_req_content) end updated_files end
updated_lockfile_content()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 70 def updated_lockfile_content @updated_lockfile_content ||= updated_generated_files.fetch(:lockfile) end
updated_pipfile_content()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 62 def updated_pipfile_content @updated_pipfile_content ||= PipfileManifestUpdater.new( dependencies: dependencies, manifest: pipfile ).updated_manifest_content end
updated_req_content()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 121 def updated_req_content updated_generated_files.fetch(:requirements_txt) end
user_specified_python_requirement()
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 359 def user_specified_python_requirement python_requirement_parser.user_specified_requirements.first end
write_temporary_dependency_files(pipfile_content)
click to toggle source
# File lib/dependabot/python/file_updater/pipfile_file_updater.rb, line 269 def write_temporary_dependency_files(pipfile_content) dependency_files.each do |file| path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) File.write(path, file.content) end # Overwrite the .python-version with updated content File.write(".python-version", python_version) setup_files.each do |file| path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) File.write(path, sanitized_setup_file_content(file)) end setup_cfg_files.each do |file| path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) File.write(path, "[metadata]\nname = sanitized-package\n") end # Overwrite the pipfile with updated content File.write("Pipfile", pipfile_content) end