diff --git a/README.md b/README.md index d2b5eb105..e519b1af7 100644 --- a/README.md +++ b/README.md @@ -133,15 +133,19 @@ For instructions on manually writing the commands and tests, see more in - [Authoring Tests](https://github.com/Azure/azure-cli/blob/dev/doc/authoring_tests.md) ## Style, linter check and testing -1. Check code style (Pylint and PEP8): +1. Auto format code (Black): + ``` + azdev format + ``` +2. Check code style (Pylint and PEP8): ``` azdev style ``` -2. Run static code checks of the CLI command table: +3. Run static code checks of the CLI command table: ``` azdev linter ``` -3. Record or replay CLI tests: +4. Record or replay CLI tests: ``` azdev test ``` diff --git a/azdev/__init__.py b/azdev/__init__.py index 19ef82112..c52cb838c 100644 --- a/azdev/__init__.py +++ b/azdev/__init__.py @@ -4,4 +4,4 @@ # license information. # ----------------------------------------------------------------------------- -__VERSION__ = '0.1.40' +__VERSION__ = '0.1.41' diff --git a/azdev/commands.py b/azdev/commands.py index 045a0d81b..0ba6da62f 100644 --- a/azdev/commands.py +++ b/azdev/commands.py @@ -21,6 +21,9 @@ def operation_group(name): with CommandGroup(self, '', operation_group('testtool')) as g: g.command('test', 'run_tests') + with CommandGroup(self, '', operation_group('format')) as g: + g.command('format', 'auto_format') + with CommandGroup(self, '', operation_group('style')) as g: g.command('style', 'check_style') diff --git a/azdev/help.py b/azdev/help.py index 39569df01..9b8e62e0e 100644 --- a/azdev/help.py +++ b/azdev/help.py @@ -99,6 +99,14 @@ """ +helps['format'] = """ + short-summary: Autoformat Python code (black). + examples: + - name: Autoformat Python code using Black. + text: azdev format +""" + + helps['style'] = """ short-summary: Check code style (pylint and PEP8). examples: diff --git a/azdev/operations/format.py b/azdev/operations/format.py new file mode 100644 index 000000000..6945fa662 --- /dev/null +++ b/azdev/operations/format.py @@ -0,0 +1,119 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- + +from glob import glob +import multiprocessing +import os +import sys + +from knack.log import get_logger +from knack.util import CLIError, CommandResultItem + +from azdev.utilities import ( + display, heading, py_cmd, get_path_table, filter_by_git_diff) + + +logger = get_logger(__name__) + + +# pylint: disable=too-many-statements +def auto_format(modules=None, git_source=None, git_target=None, git_repo=None): + + heading('Autoformat') + + # allow user to run only on CLI or extensions + cli_only = modules == ['CLI'] + ext_only = modules == ['EXT'] + if cli_only or ext_only: + modules = None + + selected_modules = get_path_table(include_only=modules) + + # remove these two non-modules + selected_modules['core'].pop('azure-cli-nspkg', None) + selected_modules['core'].pop('azure-cli-command_modules-nspkg', None) + + black_result = None + + if cli_only: + ext_names = None + selected_modules['ext'] = {} + if ext_only: + mod_names = None + selected_modules['mod'] = {} + selected_modules['core'] = {} + + # filter down to only modules that have changed based on git diff + selected_modules = filter_by_git_diff(selected_modules, git_source, git_target, git_repo) + + if not any(selected_modules.values()): + raise CLIError('No modules selected.') + + mod_names = list(selected_modules['mod'].keys()) + list(selected_modules['core'].keys()) + ext_names = list(selected_modules['ext'].keys()) + + if mod_names: + display('Modules: {}\n'.format(', '.join(mod_names))) + if ext_names: + display('Extensions: {}\n'.format(', '.join(ext_names))) + + exit_code_sum = 0 + black_result = _run_black(selected_modules) + exit_code_sum += black_result.exit_code + + if black_result.error: + logger.error(black_result.error.output.decode('utf-8')) + logger.error('Black: FAILED\n') + else: + display('Black: COMPLETE\n') + + sys.exit(exit_code_sum) + + +def _combine_command_result(cli_result, ext_result): + + final_result = CommandResultItem(None) + + def apply_result(item): + if item: + final_result.exit_code += item.exit_code + if item.error: + if final_result.error: + try: + final_result.error.message += item.error.message + except AttributeError: + final_result.error.message += str(item.error) + else: + final_result.error = item.error + setattr(final_result.error, 'message', '') + if item.result: + if final_result.result: + final_result.result += item.result + else: + final_result.result = item.result + + apply_result(cli_result) + apply_result(ext_result) + return final_result + + +def _run_black(modules): + + cli_paths = list(modules["core"].values()) + list(modules["mod"].values()) + ext_paths = list(modules["ext"].values()) + + def run(paths, desc): + if not paths: + return None + logger.debug("Running on %s:\n%s", desc, "\n".join(paths)) + command = "black -l 120 {}".format( + " ".join(paths) + ) + return py_cmd(command, message="Running black on {}...".format(desc)) + + cli_result = run(cli_paths, "modules") + ext_result = run(ext_paths, "extensions") + return _combine_command_result(cli_result, ext_result) diff --git a/azdev/operations/tests/test_format.py b/azdev/operations/tests/test_format.py new file mode 100644 index 000000000..33d66733e --- /dev/null +++ b/azdev/operations/tests/test_format.py @@ -0,0 +1,36 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- + +import configparser +import unittest +from unittest import mock + + +class TestConfigFilePath(unittest.TestCase): + def test_black_config_without_setup(self): + mocked_config = configparser.ConfigParser() + mocked_config.add_section("cli") + mocked_config.set("cli", "repo_path", "") + mocked_config.add_section("ext") + mocked_config.set("ext", "repo_paths", "") + + + def test_black_config_with_partially_setup(self): + cli_repo_path = "~/Azure/azure-cli" + mocked_config = configparser.ConfigParser() + mocked_config.add_section("cli") + mocked_config.set("cli", "repo_path", cli_repo_path) + mocked_config.add_section("ext") + mocked_config.set("ext", "repo_paths", "") + + def test_black_config_with_all_setup(self): + cli_repo_path = "~/Azure/azure-cli" + ext_repo_path = "~/Azure/azure-cli-extensions" + mocked_config = configparser.ConfigParser() + mocked_config.add_section("cli") + mocked_config.set("cli", "repo_path", cli_repo_path) + mocked_config.add_section("ext") + mocked_config.set("ext", "repo_paths", ext_repo_path) diff --git a/azdev/params.py b/azdev/params.py index d49d66508..6fc528aed 100644 --- a/azdev/params.py +++ b/azdev/params.py @@ -66,6 +66,9 @@ def load_arguments(self, _): c.argument('report', action='store_true', help='Display results as a report.') c.argument('untested_params', nargs='+', help='Space-separated list of param dest values to search for (OR logic)') + with ArgumentsContext(self, 'format') as c: + c.positional('modules', modules_type) + with ArgumentsContext(self, 'style') as c: c.positional('modules', modules_type) c.argument('pylint', action='store_true', help='Run pylint.') diff --git a/setup.py b/setup.py index e7bb18394..59c42d203 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ ], install_requires=[ 'azure-multiapi-storage', + 'black', 'docutils', 'flake8', 'gitpython',