Skip to content

Commit

Permalink
Refactor plugins command output using AirflowConsole (#13036)
Browse files Browse the repository at this point in the history
This PR refactors the airflow plugins command to be compatible with
'output' parameter which allows users to get output in form of table,
json or yaml.
  • Loading branch information
turbaszek committed Dec 13, 2020
1 parent ed4926f commit 4d3300c
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 57 deletions.
2 changes: 1 addition & 1 deletion airflow/cli/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,7 @@ class GroupCommand(NamedTuple):
name='plugins',
help='Dump information about loaded plugins',
func=lazy_load_command('airflow.cli.commands.plugins_command.dump_plugins'),
args=(),
args=(ARG_OUTPUT,),
),
GroupCommand(
name="celery",
Expand Down
65 changes: 17 additions & 48 deletions airflow/cli/commands/plugins_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,16 @@
# specific language governing permissions and limitations
# under the License.
import inspect
from typing import Any, List, Optional, Union

from rich.console import Console
from typing import Any, Dict, List, Union

from airflow import plugins_manager
from airflow.cli.simple_table import SimpleTable
from airflow.configuration import conf
from airflow.cli.simple_table import AirflowConsole
from airflow.plugins_manager import PluginsDirectorySource

# list to maintain the order of items.
from airflow.utils.cli import suppress_logs_and_warning

PLUGINS_MANAGER_ATTRIBUTES_TO_DUMP = [
"plugins",
"import_errors",
"macros_modules",
"executors_modules",
"flask_blueprints",
"flask_appbuilder_views",
"flask_appbuilder_menu_links",
"global_operator_extra_links",
"operator_extra_links",
"registered_operator_link_classes",
]
# list to maintain the order of items.
PLUGINS_ATTRIBUTES_TO_DUMP = [
"source",
"hooks",
"executors",
"macros",
Expand All @@ -49,7 +33,6 @@
"appbuilder_menu_items",
"global_operator_extra_links",
"operator_extra_links",
"source",
]


Expand Down Expand Up @@ -78,33 +61,19 @@ def dump_plugins(args):
print("No plugins loaded")
return

console = Console()
console.print("[bold yellow]SUMMARY:[/bold yellow]")
console.print(
f"[bold green]Plugins directory[/bold green]: {conf.get('core', 'plugins_folder')}\n", highlight=False
)
console.print(
f"[bold green]Loaded plugins[/bold green]: {len(plugins_manager.plugins)}\n", highlight=False
)
plugins_info: List[Dict[str, str]] = []
for plugin in plugins_manager.plugins:
info = {"name": plugin.name}
info.update({n: getattr(plugin, n) for n in PLUGINS_ATTRIBUTES_TO_DUMP})
plugins_info.append(info)

for attr_name in PLUGINS_MANAGER_ATTRIBUTES_TO_DUMP:
attr_value: Optional[List[Any]] = getattr(plugins_manager, attr_name)
if not attr_value:
continue
table = SimpleTable(title=attr_name.capitalize().replace("_", " "))
table.add_column(width=100)
for item in attr_value: # pylint: disable=not-an-iterable
table.add_row(
f"- {_get_name(item)}",
)
console.print(table)
# Remove empty info
if args.output == "table": # pylint: disable=too-many-nested-blocks
# We can do plugins_info[0] as the element it will exist as there's
# at least one plugin at this point
for col in list(plugins_info[0]):
if all(not bool(p[col]) for p in plugins_info):
for plugin in plugins_info:
del plugin[col]

console.print("[bold yellow]DETAILED INFO:[/bold yellow]")
for plugin in plugins_manager.plugins:
table = SimpleTable(title=plugin.name)
for attr_name in PLUGINS_ATTRIBUTES_TO_DUMP:
value = getattr(plugin, attr_name)
if not value:
continue
table.add_row(attr_name.capitalize().replace("_", " "), _join_plugins_names(value))
console.print(table)
AirflowConsole().print_as(plugins_info, output=args.output)
8 changes: 7 additions & 1 deletion airflow/cli/simple_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import inspect
import json
from typing import Any, Callable, Dict, List, Optional, Union

Expand All @@ -23,6 +24,8 @@
from rich.syntax import Syntax
from rich.table import Table

from airflow.plugins_manager import PluginsDirectorySource


class AirflowConsole(Console):
"""Airflow rich console"""
Expand Down Expand Up @@ -53,13 +56,16 @@ def print_as_table(self, data: List[Dict]):
table.add_row(*[str(d) for d in row.values()])
self.print(table)

def _normalize_data(self, value: Any, output: str) -> Union[list, str, dict]:
# pylint: disable=too-many-return-statements
def _normalize_data(self, value: Any, output: str) -> Optional[Union[list, str, dict]]:
if isinstance(value, (tuple, list)):
if output == "table":
return ",".join(self._normalize_data(x, output) for x in value)
return [self._normalize_data(x, output) for x in value]
if isinstance(value, dict) and output != "table":
return {k: self._normalize_data(v, output) for k, v in value.items()}
if inspect.isclass(value) and not isinstance(value, PluginsDirectorySource):
return value.__name__
if value is None:
return None
return str(value)
Expand Down
25 changes: 18 additions & 7 deletions tests/cli/commands/test_plugins_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.

import io
import json
import unittest
from contextlib import redirect_stdout

Expand Down Expand Up @@ -43,17 +44,27 @@ def setUpClass(cls):
@mock_plugin_manager(plugins=[])
def test_should_display_no_plugins(self):
with redirect_stdout(io.StringIO()) as temp_stdout:
plugins_command.dump_plugins(self.parser.parse_args(['plugins']))
plugins_command.dump_plugins(self.parser.parse_args(['plugins', '--output=json']))
stdout = temp_stdout.getvalue()
self.assertIn('No plugins loaded', stdout)

@mock_plugin_manager(plugins=[TestPlugin])
def test_should_display_one_plugins(self):
with redirect_stdout(io.StringIO()) as temp_stdout:
plugins_command.dump_plugins(self.parser.parse_args(['plugins']))
plugins_command.dump_plugins(self.parser.parse_args(['plugins', '--output=json']))
stdout = temp_stdout.getvalue()
print(stdout)
self.assertIn('Plugins directory:', stdout)
self.assertIn("Loaded plugins: 1", stdout)
self.assertIn('test-plugin-cli', stdout)
self.assertIn('PluginHook', stdout)
info = json.loads(stdout)
assert info == [
{
'name': TestPlugin.name,
'source': None,
'hooks': [PluginHook.__name__],
'executors': [],
'macros': [],
'flask_blueprints': [],
'appbuilder_views': [],
'appbuilder_menu_items': [],
'global_operator_extra_links': [],
'operator_extra_links': [],
}
]

0 comments on commit 4d3300c

Please sign in to comment.