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

fix: Improve custom commands formatters and add outputs #3228

Open
wants to merge 5 commits into
base: release/2.0
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 38 additions & 17 deletions reference/extensions/custom_commands.rst
Expand Up @@ -6,14 +6,13 @@ Custom commands
It's possible to create your own Conan commands to solve self-needs thanks to Python and Conan public API powers altogether.

Location and naming
--------------------
-------------------

All the custom commands must be located in ``[YOUR_CONAN_HOME]/extensions/commands/`` folder. If you don't know where
``[YOUR_CONAN_HOME]`` is located, you can run :command:`conan config home` to check it.
``[YOUR_CONAN_HOME]`` is located, you can run :command:`conan config home` to check it. If _commands_ sub-directory is not created yet, you will have to create it.

If _commands_ sub-directory is not created yet, you will have to create it. Those custom commands files must be Python
files and start with the prefix ``cmd_[your_command_name].py``. The call to the custom commands is like any other
existing Conan one: :command:`conan your_command_name`.
Those custom commands files must be Python files and start with the prefix ``cmd_[your_command_name].py``.
The call to the custom commands is like any other existing Conan one: :command:`conan your_command_name`.


Scoping
Expand All @@ -36,18 +35,17 @@ The call to those commands change a little bit: :command:`conan [topic_name]:you
$ conan greet:hello
$ conan greet:bye


.. note::

It's possible for only one folder layer, so it won't work to have something like
Only one folder layer is allowed. Something like this won't work:
``[YOUR_CONAN_HOME]/extensions/commands/topic1/topic2/cmd_command.py``


Decorators
-----------
----------

conan_command(group=None, formatters=None)
+++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++

Main decorator to declare a function as a new Conan command. Where the parameters are:

Expand All @@ -63,27 +61,31 @@ Main decorator to declare a function as a new Conan command. Where the parameter
import json

from conan.api.conan_api import ConanAPI
from conan.api.output import ConanOutput
from conan.api.output import cli_out_write, ConanOutput
from conan.cli.command import conan_command


def output_json(msg):
return json.dumps({"greet": msg})
cli_out_write(cli_json.dumps({"greet": msg}))

def output_text(msg):
ConanOutput().info(msg)
Comment on lines +71 to +72
Copy link
Member

Choose a reason for hiding this comment

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

output text shouldn't use ConanOutput(), that goes to sys.stderr, it should probably use cli_out_write() too

Copy link
Member Author

Choose a reason for hiding this comment

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

but ConanOutput() is kind of "non-parseable" user output right? Why should it use cli_out_write()? This is what was confusing to me and what I'd like to clarify with this PR

Copy link
Member

Choose a reason for hiding this comment

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

If it is a "formatter", it is intended for some kind of "final message" result. Even if in the text format is not machine readable, it should go to stdout, not to stderr, because it is the output of the command.

If that is not the case, and it is just informational message, it can be put in the command itself, and not in the formatter.

Copy link
Member Author

Choose a reason for hiding this comment

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

updated!

Copy link
Member

Choose a reason for hiding this comment

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

I still see the ConanOutput().info() and not cli_out_write(), did you forget to push changes?



@conan_command(group="Custom commands", formatters={"json": output_json})
@conan_command(group="Custom commands", formatters={"text": output_text,
"json": output_json})
def hello(conan_api: ConanAPI, parser, *args):
"""
Simple command to print "Hello World!" line
"""
msg = "Hello World!"
ConanOutput().info(msg)
return msg


.. important::

The function decorated by ``@conan_command(....)`` must have the same name as the suffix used by the Python file.
For instance, the previous example, the file name is ``cmd_hello.py``, and the command function decorated is ``def hello(....)``.
In the previous example, the file name is ``cmd_hello.py``, and the command function decorated is ``def hello(....)``.


conan_subcommand(formatters=None)
Expand Down Expand Up @@ -168,34 +170,53 @@ When there are sub-commands, the base command cannot define arguments, only the
Formatters
----------

The return of the command will be passed as argument to the formatters. If there are different formatters that
This allows to have different formats of output for the same command. The return of the command will be passed as argument to the formatters. If there are different formatters that
require different arguments, the approach is to return a dictionary, and let the formatters chose the
arguments they need. For example, the ``graph info`` command uses several formatters like:
arguments they need.

For example, the ``graph info`` command uses several formatters like:

.. code-block:: python

def format_graph_html(result):
def format_graph_json(result):
graph = result["graph"]
conan_api = result["conan_api"]
...
cli_out_write(cli_json.dumps(...))

def format_graph_info(result):
graph = result["graph"]
field_filter = result["field_filter"]
package_filter = result["package_filter"]
...
cli_out_write(f"Conan info result:\n\n{graph_info_result}")
...

@conan_subcommand(formatters={"text": format_graph_info,
"html": format_graph_html,
"json": format_graph_json,
"dot": format_graph_dot})
def graph_info(conan_api, parser, subparser, *args):
...
ConanOutput().info("Conan info command output:")
return {"graph": deps_graph,
"field_filter": args.filter,
"package_filter": args.package_filter,
"conan_api": conan_api}

So we can have different output formats:

```
$ conan graph info ... # Will use the formatter 'text' by default
$ conan graph info ... --format json
$ conan graph info ... --format html
```

There are two standard ways of outputing information as a result of a command:
- `cli_out_write(data, fg=None, bg=None, endline="\n", indentation=0)`: This will output information
to the `stdout`. Normally used to out put the final result of the command (like a JSON).
- `ConanOutput().info(self, msg)`: This will output information to the `stderr`. Normally used to
output informational messages and avoid cluttering the `stdout` but not the final result of the command.

Commands parameters
-------------------
Expand Down