# typed: strict# frozen_string_literal: truerequire"dependabot/dependency"require"dependabot/errors"require"dependabot/file_parsers/base/dependency_set"require"dependabot/shared_helpers"require"dependabot/uv/file_parser"require"dependabot/uv/native_helpers"require"dependabot/uv/name_normaliser"require"sorbet-runtime"moduleDependabotmoduleUvclassFileParserclassSetupFileParserextendT::SigINSTALL_REQUIRES_REGEX=/install_requires\s*=\s*\[/mSETUP_REQUIRES_REGEX=/setup_requires\s*=\s*\[/mTESTS_REQUIRE_REGEX=/tests_require\s*=\s*\[/mEXTRAS_REQUIRE_REGEX=/extras_require\s*=\s*\{/mCLOSING_BRACKET=T.let({"["=>"]","{"=>"}"}.freeze,T.any(T.untyped,T.untyped))sig{params(dependency_files: T::Array[Dependabot::DependencyFile]).void}definitialize(dependency_files:)@dependency_files=dependency_filesendsig{returns(Dependabot::FileParsers::Base::DependencySet)}defdependency_setdependencies=Dependabot::FileParsers::Base::DependencySet.newparsed_setup_file.eachdo|dep|# If a requirement has a `<` or `<=` marker then updating it is# probably blocked. Ignore it.nextifdep["markers"].include?("<")# If the requirement is our inserted version, ignore it# (we wouldn't be able to update it)nextifdep["version"]=="0.0.1+dependabot"dependencies<<Dependency.new(name: normalised_name(dep["name"],dep["extras"]),version: dep["version"]&.include?("*")?nil:dep["version"],requirements: [{requirement: dep["requirement"],file: Pathname.new(dep["file"]).cleanpath.to_path,source: nil,groups: [dep["requirement_type"]]}],package_manager: "uv")enddependenciesendprivatesig{returns(T::Array[Dependabot::DependencyFile])}attr_reader:dependency_filessig{returns(T.untyped)}defparsed_setup_fileSharedHelpers.in_a_temporary_directorydowrite_temporary_dependency_filesrequirements=SharedHelpers.run_helper_subprocess(command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",function: "parse_setup",args: [Dir.pwd])check_requirements(requirements)requirementsendrescueSharedHelpers::HelperSubprocessFailed=>eraiseDependabot::DependencyFileNotEvaluatable,e.messageife.message.start_with?("InstallationError")return[]unlesssetup_fileparsed_sanitized_setup_fileendsig{returns(T.nilable(T.any(T::Hash[String,T.untyped],String,T::Array[T::Hash[String,T.untyped]])))}defparsed_sanitized_setup_fileSharedHelpers.in_a_temporary_directorydowrite_sanitized_setup_filerequirements=SharedHelpers.run_helper_subprocess(command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",function: "parse_setup",args: [Dir.pwd])check_requirements(requirements)requirementsendrescueSharedHelpers::HelperSubprocessFailed# Assume there are no dependencies in setup.py files that fail to# parse. This isn't ideal, and we should continue to improve# parsing, but there are a *lot* of things that can go wrong at# the moment![]endsig{params(requirements: T.untyped).returns(T.untyped)}defcheck_requirements(requirements)requirements&.eachdo|dep|nextunlessdep["requirement"]Uv::Requirement.new(dep["requirement"].split(","))rescueGem::Requirement::BadRequirementError=>eraiseDependabot::DependencyFileNotEvaluatable,e.messageendendsig{void}defwrite_temporary_dependency_filesdependency_files.reject{|f|f.name==".python-version"}.eachdo|file|path=file.nameFileUtils.mkdir_p(Pathname.new(path).dirname)File.write(path,file.content)endend# Write a setup.py with only entries for the requires fields.## This sanitization is far from perfect (it will fail if any of the# entries are dynamic), but it is an alternative approach to the one# used in parser.py which sometimes succeeds when that has failed.sig{void}defwrite_sanitized_setup_fileinstall_requires=get_regexed_req_array(INSTALL_REQUIRES_REGEX)setup_requires=get_regexed_req_array(SETUP_REQUIRES_REGEX)tests_require=get_regexed_req_array(TESTS_REQUIRE_REGEX)extras_require=get_regexed_req_dict(EXTRAS_REQUIRE_REGEX)tmp="from setuptools import setup\n\n"\"setup(name=\"sanitized-package\",version=\"0.0.1\","tmp+="install_requires=#{install_requires},"ifinstall_requirestmp+="setup_requires=#{setup_requires},"ifsetup_requirestmp+="tests_require=#{tests_require},"iftests_requiretmp+="extras_require=#{extras_require},"ifextras_requiretmp+=")"File.write("setup.py",tmp)endsig{params(regex: Regexp).returns(T.nilable(String))}defget_regexed_req_array(regex)returnunless(mch=setup_file.content.match(regex))"[#{mch.post_match[0..closing_bracket_index(mch.post_match,'[')]}"endsig{params(regex: Regexp).returns(T.nilable(String))}defget_regexed_req_dict(regex)returnunless(mch=setup_file.content.match(regex))"{#{mch.post_match[0..closing_bracket_index(mch.post_match,'{')]}"endsig{params(string: String,bracket: String).returns(Integer)}defclosing_bracket_index(string,bracket)closes_required=1string.chars.each_with_indexdo|char,index|closes_required+=1ifchar==bracketcloses_required-=1ifchar==CLOSING_BRACKET.fetch(bracket)returnindexifcloses_required.zero?end0endsig{params(name: String,extras: T::Array[String]).returns(String)}defnormalised_name(name,extras)NameNormaliser.normalise_including_extras(name,extras)endsig{returns(T.untyped)}defsetup_filedependency_files.find{|f|f.name=="setup.py"}endendendendend