Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7e399fd
Document important PackagesList methods
AbrilRBS Aug 26, 2025
7ad1c0a
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Aug 27, 2025
23538dc
wip
memsharded Aug 28, 2025
cc59fb5
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 2, 2025
211f7e7
wip
memsharded Sep 2, 2025
90e5588
proposal draft
memsharded Sep 2, 2025
2ddef51
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 4, 2025
eb30098
wip
memsharded Sep 4, 2025
2c423f8
merged develop2
memsharded Sep 5, 2025
38af848
dirty, but to see if tests pass
memsharded Sep 5, 2025
75e5601
fix test
memsharded Sep 5, 2025
02508f8
remove print
memsharded Sep 5, 2025
fe8b6f5
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 9, 2025
300f442
review
memsharded Sep 9, 2025
2430ad8
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 10, 2025
fbf24ec
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 11, 2025
1f777b7
walk()->items() + accessor
memsharded Sep 11, 2025
6526420
remove private ._data access
memsharded Sep 11, 2025
4f524ae
wip
memsharded Sep 14, 2025
de79873
wip
memsharded Sep 14, 2025
3540999
wip
memsharded Sep 14, 2025
b2cfd62
wip
memsharded Sep 14, 2025
71e45b9
Some last changes to the decoumentation
AbrilRBS Sep 17, 2025
3ff9ff8
Merge branch 'develop2' into ar/packagelists-prefs
AbrilRBS Sep 17, 2025
9726b40
Update conan/api/model/list.py
memsharded Sep 17, 2025
e8c6222
Update conan/api/model/list.py
memsharded Sep 17, 2025
4704ec7
Update test/integration/command/upload/test_upload_bundle.py
memsharded Sep 17, 2025
62e6553
Merge branch 'develop2' into ar/packagelists-prefs
memsharded Sep 22, 2025
8011560
review, normalized package list name
memsharded Sep 22, 2025
843ef22
Remove last pkglist in help strings
AbrilRBS Sep 22, 2025
75c083c
Update conan/cli/commands/audit.py
memsharded Sep 22, 2025
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
23 changes: 22 additions & 1 deletion conan/api/model/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None)
)

mpkglist = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
context=base_context)
context=base_context)
if context == "build-only":
host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
context="host")
Expand Down Expand Up @@ -276,8 +276,29 @@ def refs(self):
result[recipe] = rrev_dict
return result

def items(self) -> dict[RecipeReference, dict[PkgReference, dict]]:
""" Get all the recipe references in the package list."""
result = {}
for ref, ref_dict in self.recipes.items():
for rrev, rrev_dict in ref_dict.get("revisions", {}).items():
t = rrev_dict.get("timestamp")
recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this
if t is not None:
recipe.timestamp = t

pref_dict = {}
for package_id, pkg_bundle in rrev_dict.get("packages", {}).items():
prevs = pkg_bundle.get("revisions", {})
for prev, prev_bundle in prevs.items():
t = prev_bundle.pop("timestamp", None)
pref = PkgReference(recipe, package_id, prev, t)
pref_dict[pref] = prev_bundle
result[recipe] = pref_dict
return result.items()

@staticmethod
def prefs(ref, recipe_bundle):
""" Get all the package references for a given recipe reference given a bundle."""
result = {}
for package_id, pkg_bundle in recipe_bundle.get("packages", {}).items():
prevs = pkg_bundle.get("revisions", {})
Expand Down
4 changes: 2 additions & 2 deletions conan/api/subapi/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,14 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True,
for f in backup_files:
remove(f)

for ref, ref_bundle in package_list.refs().items():
for ref, packages in package_list.items():
ConanOutput(ref.repr_notime()).verbose("Cleaning recipe cache contents")
ref_layout = cache.recipe_layout(ref)
if source:
rmdir(ref_layout.source())
if download:
rmdir(ref_layout.download_export())
for pref, _ in package_list.prefs(ref, ref_bundle).items():
for pref, _ in packages.items():
ConanOutput(pref).verbose("Cleaning package cache contents")
pref_layout = cache.pkg_layout(pref)
if build:
Expand Down
4 changes: 2 additions & 2 deletions conan/api/subapi/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ def download_full(self, package_list: PackagesList, remote: Remote,
"""Download the recipes and packages specified in the ``package_list`` from the remote,
parallelized based on ``core.download:parallel``"""
def _download_pkglist(pkglist):
for ref, recipe_bundle in pkglist.refs().items():
for ref, packages in pkglist.items():
self.recipe(ref, remote, metadata)
for pref, _ in pkglist.prefs(ref, recipe_bundle).items():
for pref, _ in packages.items():
self.package(pref, remote, metadata)

t = time.time()
Expand Down
60 changes: 30 additions & 30 deletions conan/cli/commands/remove.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from conan.api.conan_api import ConanAPI
from conan.api.model import ListPattern, MultiPackagesList
from conan.api.model import ListPattern, MultiPackagesList, RecipeReference, PkgReference
from conan.api.output import cli_out_write, ConanOutput
from conan.api.input import UserInput
from conan.cli import make_abs_path
Expand Down Expand Up @@ -92,36 +92,36 @@ def confirmation(message):
multi_package_list = MultiPackagesList()
multi_package_list.add(cache_name, package_list)

# TODO: This iteration and removal of not-confirmed is ugly and complicated, improve it
for ref, ref_bundle in package_list.refs().items():
ref_dict = package_list.recipes[str(ref)]["revisions"]
packages = ref_bundle.get("packages")
if packages is None:
if confirmation(f"Remove the recipe and all the packages of '{ref.repr_notime()}'?"):
if not args.dry_run:
conan_api.remove.recipe(ref, remote=remote)
else:
ref_dict.pop(ref.revision)
if not ref_dict:
package_list.recipes.pop(str(ref))
continue
prefs = package_list.prefs(ref, ref_bundle)
if not prefs:
ConanOutput().info(f"No binaries to remove for '{ref.repr_notime()}'")
ref_dict.pop(ref.revision)
if not ref_dict:
package_list.recipes.pop(str(ref))
continue

for pref, _ in prefs.items():
if confirmation(f"Remove the package '{pref.repr_notime()}'?"):
if not args.dry_run:
conan_api.remove.package(pref, remote=remote)
result = {}
for ref, ref_info in package_list.recipes.items():
result_ref = {}
for rrev, rrev_info in ref_info.get("revisions", {}).items():
full_ref = RecipeReference.loads(ref)
full_ref.revision = rrev
packages = rrev_info.get("packages")
if packages is None:
if confirmation(f"Remove the recipe and all the packages of '{ref}#{rrev}'?"):
if not args.dry_run:
conan_api.remove.recipe(full_ref, remote=remote)
result_ref.setdefault("revisions", {})[rrev] = rrev_info
else:
pref_dict = packages[pref.package_id]["revisions"]
pref_dict.pop(pref.revision)
if not pref_dict:
packages.pop(pref.package_id)
result_rrev = {}
for pkg_id, pkg_id_info in packages.items():
package_revisions = pkg_id_info.get("revisions")
if package_revisions is None:
ConanOutput().info(f"No binaries to remove for '{full_ref.repr_notime()}'")
continue
for prev, prev_info in package_revisions.items():
if confirmation(f"Remove the package '{ref}#{rrev}:{pkg_id}#{prev}'?"):
if not args.dry_run:
pref = PkgReference(full_ref, pkg_id, prev)
conan_api.remove.package(pref, remote=remote)
result_rrev.setdefault("packages", {})[pkg_id] = pkg_id_info
if result_rrev:
result_ref.setdefault("revisions", {})[rrev] = result_rrev
if result_ref:
result[ref] = result_ref
package_list.recipes = result

return {
"results": multi_package_list.serialize(),
Expand Down
37 changes: 20 additions & 17 deletions conan/cli/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,23 @@ def upload(conan_api: ConanAPI, parser, *args):

def _ask_confirm_upload(conan_api, package_list):
ui = UserInput(conan_api.config.get("core:non_interactive"))
for ref, bundle in package_list.refs().items():
msg = "Are you sure you want to upload recipe '%s'?" % ref.repr_notime()
ref_dict = package_list.recipes[str(ref)]["revisions"]
if not ui.request_boolean(msg):
ref_dict.pop(ref.revision)
# clean up empy refs
if not ref_dict:
package_list.recipes.pop(str(ref))
else:
for pref, prev_bundle in package_list.prefs(ref, bundle).items():
msg = "Are you sure you want to upload package '%s'?" % pref.repr_notime()
pkgs_dict = ref_dict[ref.revision]["packages"]
if not ui.request_boolean(msg):
pref_dict = pkgs_dict[pref.package_id]["revisions"]
pref_dict.pop(pref.revision)
if not pref_dict:
pkgs_dict.pop(pref.package_id)
result = {}
for ref, ref_info in package_list.recipes.items():
result_ref = {}
for rrev, rrev_info in ref_info.get("revisions", {}).items():
msg = f"Are you sure you want to upload recipe '{ref}#{rrev}'?"
if ui.request_boolean(msg):
result_rrev = {}
if rrev_info.get("timestamp"):
result_rrev["timestamp"] = rrev_info["timestamp"]
for pkg_id, pkg_id_info in rrev_info.get("packages", {}).items():
for prev, prev_info in pkg_id_info.get("revisions", {}).items():
msg = (f"Are you sure you want to upload package "
f"'{ref}#{rrev}:{pkg_id}#{prev}'?")
if ui.request_boolean(msg):
pkg_info = result_rrev.setdefault("packages", {}).setdefault(pkg_id, {})
pkg_info.setdefault("revisions", {})[prev] = prev_info
pkg_info["info"] = pkg_id_info["info"]
result_ref.setdefault("revisions", {})[rrev] = result_rrev
result[ref] = result_ref
package_list.recipes = result
33 changes: 28 additions & 5 deletions test/integration/command/list/test_combined_pkglist_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,19 +351,29 @@ def client(self):
def test_remove_nothing_only_refs(self, client):
# It is necessary to do *#* for actually removing something
client.run(f"list * --format=json", redirect_stdout="pkglist.json")
client.run(f"remove --list=pkglist.json -c")
client.run(f"remove --list=pkglist.json -c --format=json")
assert "Nothing to remove, package list do not contain recipe revisions" in client.out
result = json.loads(client.stdout)
assert result["Local Cache"] == {} # Nothing was removed

@pytest.mark.parametrize("remote", [False, True])
def test_remove_all(self, client, remote):
# It is necessary to do *#* for actually removing something
remote = "-r=default" if remote else ""
client.run(f"list *#* {remote} --format=json", redirect_stdout="pkglist.json")
client.run(f"remove --list=pkglist.json {remote} -c")
client.run(f"remove --list=pkglist.json {remote} -c --dry-run")
assert "zli/1.0.0#f034dc90894493961d92dd32a9ee3b78:" \
" Removed recipe and all binaries" in client.out
assert "zlib/1.0.0@user/channel#ffd4bc45820ddb320ab224685b9ba3fb:" \
" Removed recipe and all binaries" in client.out

client.run(f"remove --list=pkglist.json {remote} -c --format=json")
result = json.loads(client.stdout)
origin = "Local Cache" if not remote else "default"
assert len(result[origin]["zli/1.0.0"]["revisions"]) == 1
assert len(result[origin]["zlib/1.0.0@user/channel"]["revisions"]) == 1
assert "packages" not in client.stdout # Packages are not listed at all

client.run(f"list * {remote}")
assert "There are no matching recipe references" in client.out

Expand All @@ -372,22 +382,35 @@ def test_remove_packages_no_revisions(self, client, remote):
# It is necessary to do *#* for actually removing something
remote = "-r=default" if remote else ""
client.run(f"list *#*:* {remote} --format=json", redirect_stdout="pkglist.json")
client.run(f"remove --list=pkglist.json {remote} -c")
client.run(f"remove --list=pkglist.json {remote} -c --format=json")
assert "No binaries to remove for 'zli/1.0.0#f034dc90894493961d92dd32a9ee3b78'" in client.out
assert "No binaries to remove for 'zlib/1.0.0@user/channel" \
"#ffd4bc45820ddb320ab224685b9ba3fb" in client.out
result = json.loads(client.stdout)
origin = "Local Cache" if not remote else "default"
assert result[origin] == {} # Nothing was removed

@pytest.mark.parametrize("remote", [False, True])
def test_remove_packages(self, client, remote):
# It is necessary to do *#* for actually removing something
remote = "-r=default" if remote else ""
client.run(f"list *#*:*#* {remote} --format=json", redirect_stdout="pkglist.json")
client.run(f"remove --list=pkglist.json {remote} -c")

client.run(f"remove --list=pkglist.json {remote} -c --dry-run")
assert "Removed recipe and all binaries" not in client.out
assert "zli/1.0.0#f034dc90894493961d92dd32a9ee3b78: Removed binaries" in client.out
assert "zlib/1.0.0@user/channel#ffd4bc45820ddb320ab224685b9ba3fb: " \
"Removed binaries" in client.out

client.run(f"remove --list=pkglist.json {remote} -c --format=json")
result = json.loads(client.stdout)
origin = "Local Cache" if not remote else "default"
zli_revs = result[origin]["zli/1.0.0"]["revisions"]
zli_uc_revs = result[origin]["zlib/1.0.0@user/channel"]["revisions"]
assert len(zli_revs) == 1
assert len(zli_uc_revs) == 1
assert len(zli_revs["f034dc90894493961d92dd32a9ee3b78"]["packages"]) == 1
assert len(zli_uc_revs["ffd4bc45820ddb320ab224685b9ba3fb"]["packages"]) == 1

client.run(f"list *:* {remote}")
assert "zli/1.0.0" in client.out
assert "zlib/1.0.0@user/channel" in client.out
Expand Down
Loading