Skip to content

Commit

Permalink
proposal for cli-commands api (#15630)
Browse files Browse the repository at this point in the history
* proposal for cli-commands api

* subcommand

* Add test for custom command that calls the create command

* fixes

* new test to reuse other custom

---------

Co-authored-by: Luis Caro Campos <3535649+jcar87@users.noreply.github.com>
  • Loading branch information
memsharded and jcar87 authored Feb 20, 2024
1 parent fc85491 commit e2a6d96
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 0 deletions.
2 changes: 2 additions & 0 deletions conan/api/conan_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys

from conan.api.subapi.cache import CacheAPI
from conan.api.subapi.command import CommandAPI
from conan.api.subapi.local import LocalAPI
from conan.api.subapi.lockfile import LockfileAPI
from conans import __version__ as client_version
Expand Down Expand Up @@ -39,6 +40,7 @@ def __init__(self, cache_folder=None):
migrator = ClientMigrator(self.cache_folder, Version(client_version))
migrator.migrate()

self.command = CommandAPI(self)
self.remotes = RemotesAPI(self)
# Search recipes by wildcard and packages filtering by configuracion
self.search = SearchAPI(self)
Expand Down
24 changes: 24 additions & 0 deletions conan/api/subapi/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from conans.errors import ConanException


class CommandAPI:

def __init__(self, conan_api):
self.conan_api = conan_api
self.cli = None

def run(self, cmd):
if isinstance(cmd, str):
cmd = cmd.split()
if isinstance(cmd, list):
current_cmd = cmd[0]
args = cmd[1:]
else:
raise ConanException("Input of conan_api.command.run() should be a list or a string")
commands = getattr(self.cli, "_commands") # to no make it public to users of Cli class
try:
command = commands[current_cmd]
except KeyError:
raise ConanException(f"Command {current_cmd} does not exist")

return command.run_cli(self.conan_api, args)
2 changes: 2 additions & 0 deletions conan/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH = "_CONAN_INTERNAL_CUSTOM_COMMANDS_PATH"


class Cli:
"""A single command of the conan application, with all the first level commands. Manages the
parsing of parameters and delegates functionality to the conan python api. It can also show the
Expand All @@ -33,6 +34,7 @@ def __init__(self, conan_api):
assert isinstance(conan_api, ConanAPI), \
"Expected 'Conan' type, got '{}'".format(type(conan_api))
self._conan_api = conan_api
self._conan_api.command.cli = self
self._groups = defaultdict(list)
self._commands = {}

Expand Down
25 changes: 25 additions & 0 deletions conan/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ def add_subcommand(self, subcommand):
subcommand.set_name(self.name)
self._subcommands[subcommand.name] = subcommand

def run_cli(self, conan_api, *args):
parser = ConanArgumentParser(conan_api, description=self._doc,
prog="conan {}".format(self._name),
formatter_class=SmartFormatter)
self._init_log_levels(parser)
self._init_formatters(parser)
info = self._method(conan_api, parser, *args)
if not self._subcommands:
return info

subcommand_parser = parser.add_subparsers(dest='subcommand', help='sub-command help')
subcommand_parser.required = True

subcmd = args[0][0]
try:
sub = self._subcommands[subcmd]
except (KeyError, IndexError): # display help
raise ConanException(f"Sub command {subcmd} does not exist")
else:
sub.set_parser(subcommand_parser, conan_api)
return sub.run_cli(conan_api, parser, *args)

def run(self, conan_api, *args):
parser = ConanArgumentParser(conan_api, description=self._doc,
prog="conan {}".format(self._name),
Expand Down Expand Up @@ -168,6 +190,9 @@ def __init__(self, method, formatters=None):
self._parser = None
self._subcommand_name = method.__name__.replace('_', '-')

def run_cli(self, conan_api, parent_parser, *args):
return self._method(conan_api, parent_parser, self._parser, *args)

def run(self, conan_api, parent_parser, *args):
info = self._method(conan_api, parent_parser, self._parser, *args)
# It is necessary to do it after calling the "method" otherwise parser not complete
Expand Down
107 changes: 107 additions & 0 deletions conans/test/integration/command_v2/custom_commands_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import json
import os
import textwrap

import pytest

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient
from conans.util.env import environment_update
Expand Down Expand Up @@ -286,3 +290,106 @@ def hello(conan_api, parser, *args, **kwargs):
# Without the variable it only loads the default custom commands location
client.run("hello")
assert "Hello world!" in client.out


class TestCommandAPI:
@pytest.mark.parametrize("argument", ['["list", "pkg*", "-c"]',
'"list pkg* -c"'])
def test_command_reuse_interface(self, argument):
mycommand = textwrap.dedent(f"""
import json
from conan.cli.command import conan_command
from conan.api.output import cli_out_write
@conan_command(group="custom commands")
def mycommand(conan_api, parser, *args, **kwargs):
\""" mycommand help \"""
result = conan_api.command.run({argument})
cli_out_write(json.dumps(result["results"], indent=2))
""")

c = TestClient()
command_file_path = os.path.join(c.cache_folder, 'extensions',
'commands', 'cmd_mycommand.py')
c.save({f"{command_file_path}": mycommand})
c.run("mycommand", redirect_stdout="file.json")
assert json.loads(c.load("file.json")) == {"Local Cache": {}}

def test_command_reuse_other_custom(self):
cmd1 = textwrap.dedent(f"""
from conan.cli.command import conan_command
from conan.api.output import cli_out_write
@conan_command(group="custom commands")
def mycmd1(conan_api, parser, *args, **kwargs):
\"""mycommand help \"""
# result = conan_api.command.run("")
cli_out_write("MYCMD1!!!!!")
conan_api.command.run("mycmd2")
""")
cmd2 = textwrap.dedent(f"""
from conan.cli.command import conan_command
from conan.api.output import cli_out_write
@conan_command(group="custom commands")
def mycmd2(conan_api, parser, *args, **kwargs):
\"""mycommand help\"""
cli_out_write("MYCMD2!!!!!")
""")

c = TestClient()
cmds = os.path.join(c.cache_folder, 'extensions', 'commands')
c.save({os.path.join(cmds, "cmd_mycmd1.py"): cmd1,
os.path.join(cmds, "cmd_mycmd2.py"): cmd2})
c.run("mycmd1")
assert "MYCMD1!!!!!" in c.out
assert "MYCMD2!!!!!" in c.out

def test_command_reuse_interface_create(self):
mycommand = textwrap.dedent("""
import json
from conan.cli.command import conan_command
from conan.cli.formatters.graph import format_graph_json
@conan_command(group="custom commands", formatters={"json": format_graph_json})
def mycommand(conan_api, parser, *args, **kwargs):
\""" mycommand help \"""
result = conan_api.command.run(["create", ".", "--version=1.0.0"])
return result
""")

c = TestClient()
command_file_path = os.path.join(c.cache_folder, 'extensions',
'commands', 'cmd_mycommand.py')
c.save({f"{command_file_path}": mycommand,
"conanfile.py": GenConanfile("mylib")})
c.run("mycommand --format=json", redirect_stdout="file.json")
create_output = json.loads(c.load("file.json"))
assert create_output['graph']['nodes']['1']['label'] == "mylib/1.0.0"

def test_subcommand_reuse_interface(self):
mycommand = textwrap.dedent("""
import json
from conan.cli.command import conan_command
from conan.api.output import cli_out_write
@conan_command(group="custom commands")
def mycommand(conan_api, parser, *args, **kwargs):
\""" mycommand help \"""
parser.add_argument("remote", help="remote")
parser.add_argument("url", help="url")
args = parser.parse_args(*args)
conan_api.command.run(["remote", "add", args.remote, args.url])
result = conan_api.command.run(["remote", "list"])
result = {r.name: r.url for r in result}
cli_out_write(json.dumps(result, indent=2))
""")

c = TestClient()
command_file_path = os.path.join(c.cache_folder, 'extensions',
'commands', 'cmd_mycommand.py')
c.save({f"{command_file_path}": mycommand})
c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
c.run("export .")
c.run("mycommand myremote myurl")
assert json.loads(c.stdout) == {"myremote": "myurl"}

0 comments on commit e2a6d96

Please sign in to comment.