豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions python/lib/dependabot/python/file_updater/poetry_file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ def replace_dep(dep, content, new_r, old_r)
declaration_match = content.match(declaration_regex)
if declaration_match
declaration = declaration_match[:declaration]
new_declaration = T.must(declaration).sub(old_req, new_req)
return content.sub(T.must(declaration), new_declaration)
if T.must(declaration).include?(old_req)
new_declaration = T.must(declaration).sub(old_req, new_req)
return content.sub(T.must(declaration), new_declaration)
end
end

# Try Poetry table format
Expand Down Expand Up @@ -220,15 +222,13 @@ def prepared_pyproject

sig { params(pyproject_content: String).returns(String) }
def freeze_other_dependencies(pyproject_content)
PyprojectPreparer
.new(pyproject_content: pyproject_content, lockfile: lockfile)
.freeze_top_level_dependencies_except(dependencies)
PyprojectPreparer.new(pyproject_content: pyproject_content, lockfile: lockfile)
.freeze_top_level_dependencies_except(dependencies)
end

sig { params(pyproject_content: String).returns(String) }
def freeze_dependencies_being_updated(pyproject_content)
pyproject_object = TomlRB.parse(pyproject_content)

poetry_object = pyproject_object.dig("tool", "poetry")

if poetry_object
Expand All @@ -244,14 +244,18 @@ def freeze_dependencies_being_updated(pyproject_content)
end
end

# Freeze PEP 621 project.dependencies and project.optional-dependencies
PyprojectPreparer.freeze_pep621_deps!(pyproject_object, dependencies) do |dep|
!git_dependency_being_updated?(dep) && dep.version
end

TomlRB.dump(pyproject_object)
end

sig { params(pyproject_content: String).returns(String) }
def update_python_requirement(pyproject_content)
PyprojectPreparer
.new(pyproject_content: pyproject_content)
.update_python_requirement(language_version_manager.python_version)
PyprojectPreparer.new(pyproject_content: pyproject_content)
.update_python_requirement(language_version_manager.python_version)
end

sig { params(poetry_object: T::Hash[String, T.untyped], dep: Dependabot::Dependency).returns(T::Array[String]) }
Expand Down Expand Up @@ -284,9 +288,7 @@ def git_dependency_being_updated?(dep)

sig { params(pyproject_content: String).returns(String) }
def sanitize(pyproject_content)
PyprojectPreparer
.new(pyproject_content: pyproject_content)
.sanitize
PyprojectPreparer.new(pyproject_content: pyproject_content).sanitize
end

sig { params(pyproject_content: String).returns(String) }
Expand Down
78 changes: 78 additions & 0 deletions python/lib/dependabot/python/file_updater/pyproject_preparer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,46 @@ class FileUpdater
class PyprojectPreparer
extend T::Sig

# Freezes PEP 621 dependencies in-place within a parsed pyproject object.
# Replaces version specifiers with ==dep.version for each matching dep.
# Accepts an optional block to filter which dependencies to freeze.
sig do
params(
pyproject_object: T::Hash[String, T.untyped],
deps: T::Array[Dependabot::Dependency]
).void
end
def self.freeze_pep621_deps!(pyproject_object, deps)
project_object = pyproject_object["project"]
return unless project_object

dep_arrays = [project_object["dependencies"]]
project_object["optional-dependencies"]&.each_value { |opt| dep_arrays << opt }
dep_arrays.compact!

deps.each do |dep|
next if block_given? && !yield(dep)
next unless dep.version

pin_pep621_dep_in_arrays!(dep_arrays, dep)
end
end

sig { params(dep_arrays: T::Array[T::Array[String]], dep: Dependabot::Dependency).void }
def self.pin_pep621_dep_in_arrays!(dep_arrays, dep)
name_pattern = Regexp.escape(dep.name).gsub("\\-", "[-_.]")
dep_arrays.each do |arr|
arr.each_with_index do |entry, i|
next unless entry.match?(/\A#{name_pattern}(\[.*?\])?\s*[><=!~;]/i)

arr[i] = entry.sub(
/(?<pre>#{name_pattern}(?:\[.*?\])?)\s*(?:[><=!~][^;]*)/i,
"\\k<pre>==#{dep.version}"
)
end
end
Comment thread
markhallen marked this conversation as resolved.
Outdated
end

sig { params(pyproject_content: String, lockfile: T.nilable(Dependabot::DependencyFile)).void }
def initialize(pyproject_content:, lockfile: nil)
@pyproject_content = pyproject_content
Expand Down Expand Up @@ -109,6 +149,9 @@ def freeze_top_level_dependencies_except(dependencies)
end
end

# Freeze PEP 621 project.dependencies and project.optional-dependencies
freeze_pep621_top_level_deps!(pyproject_object, excluded_names)

TomlRB.dump(pyproject_object)
end
# rubocop:enable Metrics/AbcSize
Expand All @@ -122,6 +165,41 @@ def freeze_top_level_dependencies_except(dependencies)
sig { returns(T.nilable(Dependabot::DependencyFile)) }
attr_reader :lockfile

sig { params(pyproject_object: T::Hash[String, T.untyped], excluded_names: T::Array[String]).void }
def freeze_pep621_top_level_deps!(pyproject_object, excluded_names)
project_object = pyproject_object["project"]
return unless project_object

freeze_pep621_dep_array!(project_object["dependencies"], excluded_names)

project_object["optional-dependencies"]&.each_value do |opt_deps|
freeze_pep621_dep_array!(opt_deps, excluded_names)
end
end

sig { params(dep_array: T.nilable(T::Array[String]), excluded_names: T::Array[String]).void }
def freeze_pep621_dep_array!(dep_array, excluded_names)
return unless dep_array

dep_array.each_with_index do |entry, index|
# Extract dependency name from PEP 508 string
match = entry.match(/\A([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)/i)
next unless match

dep_name = normalise(match[1])
next if excluded_names.include?(dep_name)

locked_details = locked_details(dep_name)
next unless (locked_version = locked_details&.fetch("version"))

# Replace version specifier with pinned version, preserving markers
dep_array[index] = entry.sub(
/(?<name_and_extras>[a-zA-Z0-9][a-zA-Z0-9._-]*(?:\[.*?\])?)\s*(?<specifier>[><=!~][^;]*)/i,
"\\k<name_and_extras>==#{locked_version}"
Comment thread
markhallen marked this conversation as resolved.
Outdated
)
end
end

sig { params(dep_name: String).returns(T.nilable(T::Hash[String, T.untyped])) }
def locked_details(dep_name)
parsed_lockfile.fetch("package")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,115 @@
end
end

describe "hybrid Poetry v2 projects" do
let(:dependency_files) { [pyproject] }

context "when dependency has version in both project.dependencies and tool.poetry.dependencies" do
let(:pyproject_fixture_name) { "pep621_hybrid_version_in_both.toml" }
let(:dependency) do
Dependabot::Dependency.new(
name: "requests",
version: "2.19.1",
previous_version: "2.13.0",
package_manager: "pip",
requirements: [
{
requirement: ">=2.19.1",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
},
{
requirement: "^2.19",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
}
],
previous_requirements: [
{
requirement: ">=2.13.0",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
},
{
requirement: "^2.13",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
}
]
)
end

describe "#updated_dependency_files" do
subject(:updated_files) { updater.updated_dependency_files }

it "updates the version in project.dependencies" do
updated_pyproject = updated_files.find { |f| f.name == "pyproject.toml" }
expect(updated_pyproject.content).to include('"requests>=2.19.1"')
expect(updated_pyproject.content).not_to include('"requests>=2.13.0"')
end

it "updates the version in tool.poetry.dependencies" do
updated_pyproject = updated_files.find { |f| f.name == "pyproject.toml" }
parsed = TomlRB.parse(updated_pyproject.content)
poetry_req = parsed.dig("tool", "poetry", "dependencies", "requests")
expect(poetry_req["version"]).to eq("^2.19")
end

it "preserves enrichment metadata in tool.poetry.dependencies" do
updated_pyproject = updated_files.find { |f| f.name == "pyproject.toml" }
parsed = TomlRB.parse(updated_pyproject.content)
poetry_req = parsed.dig("tool", "poetry", "dependencies", "requests")
expect(poetry_req["source"]).to eq("private-source")
end
end
end

context "when tool.poetry.dependencies has enrichment only (no version key)" do
let(:pyproject_fixture_name) { "pep621_hybrid_enrichment_only.toml" }
let(:dependency) do
Dependabot::Dependency.new(
name: "requests",
version: "2.19.1",
previous_version: "2.13.0",
package_manager: "pip",
requirements: [{
requirement: ">=2.19.1",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
}],
previous_requirements: [{
requirement: ">=2.13.0",
file: "pyproject.toml",
source: nil,
groups: ["dependencies"]
}]
)
end

describe "#updated_dependency_files" do
subject(:updated_files) { updater.updated_dependency_files }

it "updates the version in project.dependencies" do
updated_pyproject = updated_files.find { |f| f.name == "pyproject.toml" }
expect(updated_pyproject.content).to include('"requests>=2.19.1"')
expect(updated_pyproject.content).not_to include('"requests>=2.13.0"')
end

it "leaves the enrichment-only entry in tool.poetry.dependencies unchanged" do
updated_pyproject = updated_files.find { |f| f.name == "pyproject.toml" }
parsed = TomlRB.parse(updated_pyproject.content)
poetry_req = parsed.dig("tool", "poetry", "dependencies", "requests")
expect(poetry_req).to eq({ "source" => "private-source" })
end
end
end
end

describe "#prepared_project_file" do
subject(:prepared_project) { updater.send(:prepared_pyproject) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,47 @@

it { is_expected.to include("subdirectory = \"python\"\n") }
end

context "with PEP 621 project.dependencies" do
let(:dependencies) { [] }
let(:pyproject_fixture_name) { "pep621_hybrid_version_in_both.toml" }
let(:poetry_lock_fixture_name) { "caret_version.lock" }

it "freezes PEP 621 dependencies to their locked versions" do
result = freeze_top_level_dependencies_except
parsed = TomlRB.parse(result)
project_deps = parsed.dig("project", "dependencies")
requests_dep = project_deps.find { |d| d.start_with?("requests") }
expect(requests_dep).to eq("requests==1.2.3")
end

it "also freezes tool.poetry.dependencies" do
result = freeze_top_level_dependencies_except
parsed = TomlRB.parse(result)
poetry_req = parsed.dig("tool", "poetry", "dependencies", "requests")
expect(poetry_req["version"]).to eq("1.2.3")
end

context "when excluding a dependency" do
let(:dependencies) do
[
Dependabot::Dependency.new(
name: "requests",
version: "2.19.1",
package_manager: "pip",
requirements: []
)
]
end

it "does not freeze the excluded PEP 621 dependency" do
result = freeze_top_level_dependencies_except
parsed = TomlRB.parse(result)
project_deps = parsed.dig("project", "dependencies")
requests_dep = project_deps.find { |d| d.start_with?("requests") }
expect(requests_dep).to eq("requests>=2.13.0")
end
end
Comment thread
markhallen marked this conversation as resolved.
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "test"
version = "0.1.0"
description = ""
requires-python = ">=3.9"

dependencies = [
"requests>=2.13.0",
]

[tool.poetry]
package-mode = false

[tool.poetry.dependencies]
requests = { source = "private-source" }

[[tool.poetry.source]]
name = "private-source"
url = "https://private.example.com/simple"
priority = "supplemental"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "test"
version = "0.1.0"
description = ""
requires-python = ">=3.9"

dependencies = [
"requests>=2.13.0",
]

[tool.poetry]
package-mode = false

[tool.poetry.dependencies]
requests = { version = "^2.13", source = "private-source" }

[[tool.poetry.source]]
name = "private-source"
url = "https://private.example.com/simple"
priority = "supplemental"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Loading