Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

conan remove pkg-lists support #14082

Merged
merged 4 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion conan/api/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def load(file):
result[remote] = PackagesList.deserialize(pkglist)
pkglist = MultiPackagesList()
pkglist.lists = result
return result
return pkglist

@staticmethod
def load_graph(graphfile, graph_recipes=None, graph_binaries=None):
Expand Down
76 changes: 60 additions & 16 deletions conan/cli/commands/remove.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
from conan.api.conan_api import ConanAPI
from conan.api.model import ListPattern
from conan.api.model import ListPattern, MultiPackagesList
from conan.api.output import cli_out_write, ConanOutput
from conan.cli import make_abs_path
from conan.cli.command import conan_command, OnceArgument
from conan.cli.commands.list import print_list_json, print_serial
from conans.client.userio import UserInput
from conans.errors import ConanException


@conan_command(group="Consumer")
def summary_remove_list(results):
""" Do litte format modification to serialized
list bundle so it looks prettier on text output
"""
cli_out_write("Remove summary:")
info = results["results"]
result = {}
for remote, remote_info in info.items():
new_info = result.setdefault(remote, {})
for ref, content in remote_info.items():
for rev, rev_content in content.get("revisions", {}).items():
pkgids = rev_content.get('packages')
if pkgids is None:
new_info.setdefault(f"{ref}#{rev}", "Removed recipe and all binaries")
else:
new_info.setdefault(f"{ref}#{rev}", f"Removed binaries: {list(pkgids)}")
print_serial(result)


@conan_command(group="Consumer", formatters={"text": summary_remove_list,
"json": print_list_json})
def remove(conan_api: ConanAPI, parser, *args):
"""
Remove recipes or packages from local cache or a remote.
Expand All @@ -16,7 +39,7 @@ def remove(conan_api: ConanAPI, parser, *args):
will be removed.
- If a package reference is specified, it will remove only the package.
"""
parser.add_argument('pattern',
parser.add_argument('pattern', nargs="?",
help="A pattern in the form 'pkg/version#revision:package_id#revision', "
"e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
"If revision is not specified, it is assumed latest one.")
Expand All @@ -27,26 +50,47 @@ def remove(conan_api: ConanAPI, parser, *args):
"os=Windows AND (arch=x86 OR compiler=gcc)")
parser.add_argument('-r', '--remote', action=OnceArgument,
help='Will remove from the specified remote')
parser.add_argument("-l", "--list", help="Package list file")
args = parser.parse_args(*args)

if args.pattern is None and args.list is None:
raise ConanException("Missing pattern or package list file")
if args.pattern and args.list:
raise ConanException("Cannot define both the pattern and the package list file")
if args.package_query and args.list:
raise ConanException("Cannot define package-query and the package list file")

ui = UserInput(conan_api.config.get("core:non_interactive"))
remote = conan_api.remotes.get(args.remote) if args.remote else None

def confirmation(message):
return args.confirm or ui.request_boolean(message)

ref_pattern = ListPattern(args.pattern, rrev="*", prev="*")
select_bundle = conan_api.list.select(ref_pattern, args.package_query, remote)

if ref_pattern.package_id is None:
if args.package_query is not None:
if args.list:
listfile = make_abs_path(args.list)
multi_package_list = MultiPackagesList.load(listfile)
package_list = multi_package_list["Local Cache" if not remote else remote.name]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe time to move "Local Cache" to some kind of constant? (Not on this PR) It's in the codebase a few times by now

refs_to_remove = package_list.refs()
if not refs_to_remove: # the package list might contain only refs, no revs
ConanOutput().warning("Nothing to remove, package list do not contain recipe revisions")
else:
ref_pattern = ListPattern(args.pattern, rrev="*", prev="*")
if ref_pattern.package_id is None and args.package_query is not None:
raise ConanException('--package-query supplied but the pattern does not match packages')
for ref, _ in select_bundle.refs():
if confirmation("Remove the recipe and all the packages of '{}'?"
"".format(ref.repr_notime())):
package_list = conan_api.list.select(ref_pattern, args.package_query, remote)
multi_package_list = MultiPackagesList()
multi_package_list.add("Local Cache" if not remote else remote.name, package_list)

for ref, ref_bundle in package_list.refs():
if ref_bundle.get("packages") is None:
if confirmation(f"Remove the recipe and all the packages of '{ref.repr_notime()}'?"):
conan_api.remove.recipe(ref, remote=remote)
else:
for ref, ref_bundle in select_bundle.refs():
for pref, _ in select_bundle.prefs(ref, ref_bundle):
if confirmation("Remove the package '{}'?".format(pref.repr_notime())):
conan_api.remove.package(pref, remote=remote)
continue
for pref, _ in package_list.prefs(ref, ref_bundle):
if confirmation(f"Remove the package '{pref.repr_notime()}'?"):
conan_api.remove.package(pref, remote=remote)

return {
"results": multi_package_list.serialize(),
"conan_api": conan_api
}
14 changes: 8 additions & 6 deletions conan/cli/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ def summary_upload_list(results):
"""
cli_out_write("Upload summary:")
info = results["results"]
new_info = {}
result = {}
for remote, remote_info in info.items():
new_info = result.setdefault(remote, {})
for ref, content in remote_info.items():
for rev, rev_content in content["revisions"].items():
prevs = rev_content.get('packages')
if prevs:
new_info.setdefault(f"{ref}:{rev}", list(prevs))
info = new_info
print_serial(info)
pkgids = rev_content.get('packages')
if pkgids:
new_info.setdefault(f"{ref}:{rev}", list(pkgids))
print_serial(result)


@conan_command(group="Creator", formatters={"text": summary_upload_list,
Expand Down Expand Up @@ -71,6 +71,8 @@ def upload(conan_api: ConanAPI, parser, *args):
raise ConanException("Missing pattern or package list file")
if args.pattern and args.list:
raise ConanException("Cannot define both the pattern and the package list file")
if args.package_query and args.list:
raise ConanException("Cannot define package-query and the package list file")
if args.list:
listfile = make_abs_path(args.list)
multi_package_list = MultiPackagesList.load(listfile)
Expand Down
48 changes: 48 additions & 0 deletions conans/test/integration/command_v2/test_combined_pkglist_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,51 @@ def test_download_upload_only_recipes(self, client, prev_list):
assert f"Uploading recipe 'zli/" not in client.out
assert "Uploading package 'zlib/1.0.0" not in client.out
assert "Uploading package 'zli/" not in client.out


class TestListRemove:
@pytest.fixture()
def client(self):
c = TestClient(default_server_user=True)
c.save({
"zlib.py": GenConanfile("zlib"),
"zli.py": GenConanfile("zli", "1.0.0")
})
c.run("create zli.py")
c.run("create zlib.py --version=1.0.0 --user=user --channel=channel")
c.run("upload * -r=default -c")
return c

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")
memsharded marked this conversation as resolved.
Show resolved Hide resolved
client.run(f"remove --list=pkglist.json -c")
assert "Nothing to remove, package list do not contain recipe revisions" in client.out

@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")
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"list * {remote}")
assert "There are no matching recipe references" in client.out

@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")
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"list *:* {remote}")
assert "zli/1.0.0" in client.out
assert "zlib/1.0.0@user/channel" in client.out