Skip to content

Commit 0d00d60

Browse files
Merge branch 'main' into harry/add-support-callable-workflows
2 parents 39e1a45 + 35d4355 commit 0d00d60

File tree

15 files changed

+365
-308
lines changed

15 files changed

+365
-308
lines changed

nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/FileWriters/FileWriterWorkerTests.cs

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,12 @@ await TestAsync(
812812
discoveryWorker: new TestDiscoveryWorker(args =>
813813
{
814814
discoveryRequestCount++;
815-
var result = discoveryRequestCount switch
815+
if (discoveryRequestCount <= 3)
816816
{
817-
// initial request, report 1.0.0
818-
1 => new WorkspaceDiscoveryResult()
817+
// 1 - initial request
818+
// 2 - pre-edit request
819+
// 3 - post-edit request - no change made, indicates failure
820+
return Task.FromResult(new WorkspaceDiscoveryResult()
819821
{
820822
Path = "/",
821823
Projects = [
@@ -829,26 +831,10 @@ await TestAsync(
829831
ReferencedProjectPaths = []
830832
}
831833
]
832-
},
833-
// post-edit request, report 1.0.0 again, indicating the file edits didn't produce the desired result
834-
2 => new WorkspaceDiscoveryResult()
835-
{
836-
Path = "/",
837-
Projects = [
838-
new ProjectDiscoveryResult()
839-
{
840-
FilePath = "project.csproj",
841-
Dependencies = [new Dependency("Some.Dependency", "1.0.0", DependencyType.PackageReference)],
842-
TargetFrameworks = ["net9.0"],
843-
AdditionalFiles = [],
844-
ImportedFiles = [],
845-
ReferencedProjectPaths = []
846-
}
847-
]
848-
},
849-
_ => throw new NotSupportedException($"Didn't expect {discoveryRequestCount} discovery requests"),
850-
};
851-
return Task.FromResult(result);
834+
});
835+
}
836+
837+
throw new NotSupportedException($"Didn't expect {discoveryRequestCount} discovery requests");
852838
}),
853839
dependencySolver: null, // use real worker
854840
fileWriter: null, // use real worker
@@ -867,6 +853,80 @@ await TestAsync(
867853
);
868854
}
869855

856+
[Fact]
857+
public async Task EndToEnd_PriorFileEditResolvedDependencyInSubsequentFile()
858+
{
859+
// via a ProjectReference, two projects have the same dependency and updating the root causes the other dependency to also be updated and not result in unnecessarily pinning anything
860+
await TestAsync(
861+
dependencyName: "Some.Dependency",
862+
oldDependencyVersion: "1.0.0",
863+
newDependencyVersion: "2.0.0",
864+
files: [
865+
("src/a/a.csproj", """
866+
<Project Sdk="Microsoft.NET.Sdk">
867+
<PropertyGroup>
868+
<TargetFramework>net9.0</TargetFramework>
869+
</PropertyGroup>
870+
<ItemGroup>
871+
<ProjectReference Include="..\b\b.csproj" />
872+
</ItemGroup>
873+
<ItemGroup>
874+
<PackageReference Include="Unrelated.Dependency" Version="3.0.0" />
875+
</ItemGroup>
876+
</Project>
877+
"""),
878+
("src/b/b.csproj", """
879+
<Project Sdk="Microsoft.NET.Sdk">
880+
<PropertyGroup>
881+
<TargetFramework>net9.0</TargetFramework>
882+
</PropertyGroup>
883+
<ItemGroup>
884+
<PackageReference Include="Some.Dependency" Version="1.0.0" />
885+
</ItemGroup>
886+
</Project>
887+
"""),
888+
("Directory.Build.props", "<Project />"),
889+
("Directory.Build.targets", "<Project />"),
890+
],
891+
packages: [
892+
MockNuGetPackage.CreateSimplePackage("Some.Dependency", "1.0.0", "net9.0"),
893+
MockNuGetPackage.CreateSimplePackage("Some.Dependency", "2.0.0", "net9.0"),
894+
MockNuGetPackage.CreateSimplePackage("Unrelated.Dependency", "3.0.0", "net9.0"),
895+
],
896+
discoveryWorker: null, // use real worker
897+
dependencySolver: null, // use real worker
898+
fileWriter: null, // use real worker
899+
expectedFiles: [
900+
("src/a/a.csproj", """
901+
<Project Sdk="Microsoft.NET.Sdk">
902+
<PropertyGroup>
903+
<TargetFramework>net9.0</TargetFramework>
904+
</PropertyGroup>
905+
<ItemGroup>
906+
<ProjectReference Include="..\b\b.csproj" />
907+
</ItemGroup>
908+
<ItemGroup>
909+
<PackageReference Include="Unrelated.Dependency" Version="3.0.0" />
910+
</ItemGroup>
911+
</Project>
912+
"""),
913+
("src/b/b.csproj", """
914+
<Project Sdk="Microsoft.NET.Sdk">
915+
<PropertyGroup>
916+
<TargetFramework>net9.0</TargetFramework>
917+
</PropertyGroup>
918+
<ItemGroup>
919+
<PackageReference Include="Some.Dependency" Version="2.0.0" />
920+
</ItemGroup>
921+
</Project>
922+
"""),
923+
],
924+
expectedOperations: [
925+
new PinnedUpdate() { DependencyName = "Some.Dependency", NewVersion = NuGetVersion.Parse("2.0.0"), UpdatedFiles = ["/src/b/b.csproj"] }
926+
]
927+
);
928+
}
929+
870930
private static async Task TestAsync(
871931
string dependencyName,
872932
string oldDependencyVersion,

nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/FileWriters/FileWriterWorker.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,62 @@ NuGetVersion newDependencyVersion
209209
foreach (var projectDiscovery in orderedProjectDiscovery)
210210
{
211211
var projectFullPath = Path.Join(repoContentsPath.FullName, initialDiscoveryResult.Path, projectDiscovery.FilePath).FullyNormalizedRootedPath();
212-
var updatedFiles = await TryPerformFileWritesAsync(_fileWriter, repoContentsPath, initialProjectDirectory, projectDiscovery, resolvedDependencies.Value);
212+
var projectDirectory = new DirectoryInfo(Path.GetDirectoryName(projectFullPath)!);
213+
var projectRelativePath = Path.GetRelativePath(repoContentsPath.FullName, projectFullPath).FullyNormalizedRootedPath();
214+
var projectRelativeDirectory = Path.GetDirectoryName(projectRelativePath)!.NormalizePathToUnix();
215+
_logger.Info($"Attempting to update {dependencyName} for {projectRelativePath}");
216+
217+
// rerun discovery because a previous file update may have already fixed this
218+
var rerunWorkspaceDiscovery = await _discoveryWorker.RunAsync(repoContentsPath.FullName, projectRelativeDirectory);
219+
var rerunProjectDiscovery = rerunWorkspaceDiscovery.GetProjectDiscoveryFromFullPath(repoContentsPath, new FileInfo(projectFullPath));
220+
if (rerunProjectDiscovery is null)
221+
{
222+
_logger.Warn($" Unable to re-run project discovery for project {projectRelativePath}.");
223+
continue;
224+
}
225+
226+
var candidateDependencyToUpdate = rerunProjectDiscovery.Dependencies.FirstOrDefault(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase));
227+
if (candidateDependencyToUpdate?.Version is null)
228+
{
229+
_logger.Warn($" Unable to find dependency after discovery rerun.");
230+
continue;
231+
}
232+
233+
if (!NuGetVersion.TryParse(candidateDependencyToUpdate.Version, out var candidateDependencyCurrentVersion))
234+
{
235+
_logger.Warn($" Unable to parse discovered version number from string: {candidateDependencyToUpdate.Version}");
236+
continue;
237+
}
238+
239+
if (candidateDependencyCurrentVersion >= newDependencyVersion)
240+
{
241+
_logger.Info($" Dependency is already up to date at version {candidateDependencyCurrentVersion}, possibly from a previous operation.");
242+
continue;
243+
}
244+
245+
var rerunTopLevelDependencies = rerunProjectDiscovery.Dependencies
246+
.Where(d => !d.IsTransitive)
247+
.ToImmutableArray();
248+
var rerunDesiredDependencies = rerunTopLevelDependencies.Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))
249+
? rerunTopLevelDependencies.Select(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase) ? newDependency : d).ToImmutableArray()
250+
: rerunTopLevelDependencies.Concat([newDependency]).ToImmutableArray();
251+
var resolvedDependenciesInThisproject = await _dependencySolver.SolveAsync(rerunTopLevelDependencies, rerunDesiredDependencies, targetFramework);
252+
if (resolvedDependenciesInThisproject is null)
253+
{
254+
_logger.Warn($" Unable to solve dependency conflicts for {projectRelativePath}/{targetFramework}.");
255+
continue;
256+
}
257+
258+
var updatedFiles = await TryPerformFileWritesAsync(_fileWriter, repoContentsPath, projectDirectory, rerunProjectDiscovery!, resolvedDependenciesInThisproject.Value);
259+
if (updatedFiles.Length == 0)
260+
{
261+
_logger.Info(" Files were unable to be updated.");
262+
}
263+
else
264+
{
265+
_logger.Info($" Successfully updated the following files: {string.Join(", ", updatedFiles)}");
266+
}
267+
213268
allUpdatedFiles.AddRange(updatedFiles);
214269
}
215270

python/helpers/lib/parser.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def version_from_req(specifier_set):
3232
next(iter(specifier_set)).operator in {"==", "==="}):
3333
return next(iter(specifier_set)).version
3434

35-
def parse_requirement(entry, pyproject_path):
35+
def parse_requirement(entry, pyproject_path, requirement_type=None):
3636
try:
3737
req = Requirement(entry)
3838
except InvalidRequirement as e:
@@ -46,14 +46,19 @@ def parse_requirement(entry, pyproject_path):
4646
"file": pyproject_path,
4747
"requirement": str(req.specifier),
4848
"extras": sorted(list(req.extras)),
49+
"requirement_type": requirement_type,
4950
}
5051
return data
5152

52-
def parse_toml_section_pep621_dependencies(pyproject_path, dependencies):
53+
def parse_toml_section_pep621_dependencies(
54+
pyproject_path, dependencies, requirement_type=None
55+
):
5356
requirement_packages = []
5457

5558
for dependency in dependencies:
56-
parsed_dependency = parse_requirement(dependency, pyproject_path)
59+
parsed_dependency = parse_requirement(
60+
dependency, pyproject_path, requirement_type
61+
)
5762
requirement_packages.append(parsed_dependency)
5863

5964
return requirement_packages
@@ -75,7 +80,9 @@ def parse_toml_section_pep735_dependencies(
7580
for entry in dependencies:
7681
# Handle direct requirement
7782
if isinstance(entry, str):
78-
parsed_dependency = parse_requirement(entry, pyproject_path)
83+
parsed_dependency = parse_requirement(
84+
entry, pyproject_path, group_name
85+
)
7986
requirement_packages.append(parsed_dependency)
8087
# Handle include-group directive
8188
elif isinstance(entry, dict) and "include-group" in entry:
@@ -100,7 +107,8 @@ def parse_toml_section_pep735_dependencies(
100107
dependencies_toml = project_section['dependencies']
101108
runtime_dependencies = parse_toml_section_pep621_dependencies(
102109
pyproject_path,
103-
dependencies_toml
110+
dependencies_toml,
111+
"dependencies"
104112
)
105113
dependencies.extend(runtime_dependencies)
106114

@@ -111,7 +119,8 @@ def parse_toml_section_pep735_dependencies(
111119
for group in optional_dependencies_toml:
112120
group_dependencies = parse_toml_section_pep621_dependencies(
113121
pyproject_path,
114-
optional_dependencies_toml[group]
122+
optional_dependencies_toml[group],
123+
group
115124
)
116125
dependencies.extend(group_dependencies)
117126

@@ -128,7 +137,8 @@ def parse_toml_section_pep735_dependencies(
128137
if 'requires' in build_system_section:
129138
build_system_dependencies = parse_toml_section_pep621_dependencies(
130139
pyproject_path,
131-
build_system_section['requires']
140+
build_system_section['requires'],
141+
"build-system.requires"
132142
)
133143
dependencies.extend(build_system_dependencies)
134144

python/lib/dependabot/python/file_parser/pyproject_files_parser.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ def dependency_set
4343

4444
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
4545
def pyproject_dependencies
46-
if using_poetry?
47-
poetry_dependencies
48-
else
49-
pep621_pep735_dependencies
50-
end
46+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
47+
48+
# Parse Poetry dependencies if [tool.poetry] section exists
49+
dependencies += poetry_dependencies if using_poetry?
50+
51+
# Parse PEP 621/735 dependencies if those sections exist
52+
# This handles hybrid projects that have both Poetry and PEP 621 sections
53+
dependencies += pep621_pep735_dependencies if using_pep621? || using_pep735?
54+
55+
dependencies
5156
end
5257

5358
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
@@ -84,11 +89,15 @@ def pep621_pep735_dependencies
8489
parse_pep621_pep735_dependencies.each do |dep|
8590
# If a requirement has a `<` or `<=` marker then updating it is
8691
# probably blocked. Ignore it.
87-
next if dep["markers"].include?("<")
92+
next if dep["markers"]&.include?("<")
8893

8994
# If no requirement, don't add it
9095
next if dep["requirement"].empty?
9196

97+
# Skip build-system.requires dependencies when using Poetry
98+
# Poetry manages its own build system dependencies
99+
next if using_poetry? && dep["requirement_type"] == "build-system.requires"
100+
92101
dependencies <<
93102
Dependency.new(
94103
name: normalised_name(dep["name"], dep["extras"]),

python/lib/dependabot/python/update_checker.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ def subdependency_resolver
181181

182182
sig { returns(Symbol) }
183183
def pyproject_resolver
184-
return :poetry if poetry_based?
184+
# For hybrid projects with both [tool.poetry] and [project] sections but no lockfile,
185+
# use the requirements resolver to handle PEP 621 dependencies
186+
# For pure Poetry projects, use Poetry resolver even without lockfile
187+
return :poetry if poetry_based? && (poetry_lock || !standard_details)
185188

186189
:requirements
187190
end

python/spec/dependabot/python/file_parser/pyproject_files_parser_spec.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@
276276
[{
277277
requirement: "==0.3.0",
278278
file: "pyproject.toml",
279-
groups: [],
279+
groups: ["dependencies"],
280280
source: nil
281281
}]
282282
)
@@ -394,6 +394,26 @@
394394

395395
its(:length) { is_expected.to be > 0 }
396396
end
397+
398+
context "with PEP 621 and Poetry configuration" do
399+
subject(:dependencies) { parser.dependency_set.dependencies }
400+
401+
let(:pyproject_fixture_name) { "pep621_with_poetry.toml" }
402+
403+
its(:length) { is_expected.to eq(2) }
404+
405+
it "has the correct dependencies with requirement types" do
406+
expect(dependencies.map(&:name)).to contain_exactly("fastapi", "pydantic")
407+
408+
fastapi = dependencies.find { |d| d.name == "fastapi" }
409+
expect(fastapi.version).to eq("0.115.5")
410+
expect(fastapi.requirements.first[:groups]).to eq(["dependencies"])
411+
412+
pydantic = dependencies.find { |d| d.name == "pydantic" }
413+
expect(pydantic.version).to eq("2.8.2")
414+
expect(pydantic.requirements.first[:groups]).to eq(["dependencies"])
415+
end
416+
end
397417
end
398418

399419
describe "with pep 735" do

python/spec/dependabot/python/update_checker_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,47 @@
447447
.to eq(Gem::Version.new("2.5.0"))
448448
end
449449
end
450+
451+
context "when including hybrid Poetry + PEP621 dependencies without lockfile" do
452+
let(:pyproject_fixture_name) { "pep621_with_poetry.toml" }
453+
let(:dependency_files) { [pyproject] }
454+
455+
it "delegates to PipVersionResolver for PEP 621 dependencies" do
456+
dummy_resolver =
457+
instance_double(described_class::PipVersionResolver)
458+
allow(described_class::PipVersionResolver).to receive(:new)
459+
.and_return(dummy_resolver)
460+
expect(dummy_resolver)
461+
.to receive(:latest_resolvable_version)
462+
.and_return(Gem::Version.new("2.5.0"))
463+
expect(checker.latest_resolvable_version)
464+
.to eq(Gem::Version.new("2.5.0"))
465+
end
466+
end
467+
468+
context "when including hybrid Poetry + PEP621 dependencies with lockfile" do
469+
let(:pyproject_fixture_name) { "poetry_exact_requirement.toml" }
470+
let(:poetry_lock) do
471+
Dependabot::DependencyFile.new(
472+
name: "poetry.lock",
473+
content: fixture("poetry_locks", "exact_version.lock")
474+
)
475+
end
476+
let(:dependency_files) { [pyproject, poetry_lock] }
477+
478+
it "delegates to PoetryVersionResolver when lockfile exists" do
479+
dummy_resolver =
480+
instance_double(described_class::PoetryVersionResolver)
481+
allow(described_class::PoetryVersionResolver).to receive(:new)
482+
.and_return(dummy_resolver)
483+
expect(dummy_resolver)
484+
.to receive(:latest_resolvable_version)
485+
.with(requirement: ">=2.0.0,<=2.6.0")
486+
.and_return(Gem::Version.new("2.5.0"))
487+
expect(checker.latest_resolvable_version)
488+
.to eq(Gem::Version.new("2.5.0"))
489+
end
490+
end
450491
end
451492
end
452493

0 commit comments

Comments
 (0)