From 3025f67d370e02ebae075a4ef68e1d7f0461b2d6 Mon Sep 17 00:00:00 2001 From: Jiajie Zhong Date: Thu, 24 Feb 2022 16:05:55 +0800 Subject: [PATCH] [python] Add mechanism cli only with version as subcommand (#8516) * Add basic cli mechanism with Click, for now just including one single subcommand `version` * Add general and easy test class in tests/testing/cli, and test to version * Add sphinx-click to general cli docs basic on click --- .../pydolphinscheduler/README.md | 4 +- .../pydolphinscheduler/docs/source/cli.rst | 36 ++++++++ .../pydolphinscheduler/docs/source/conf.py | 2 + .../pydolphinscheduler/docs/source/index.rst | 1 + .../pydolphinscheduler/setup.py | 7 ++ .../src/pydolphinscheduler/cli/__init__.py | 18 ++++ .../src/pydolphinscheduler/cli/commands.py | 48 ++++++++++ .../pydolphinscheduler/tests/cli/__init__.py | 18 ++++ .../tests/cli/test_version.py | 59 ++++++++++++ .../pydolphinscheduler/tests/testing/cli.py | 91 +++++++++++++++++++ 10 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 dolphinscheduler-python/pydolphinscheduler/docs/source/cli.rst create mode 100644 dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/__init__.py create mode 100644 dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py create mode 100644 dolphinscheduler-python/pydolphinscheduler/tests/cli/__init__.py create mode 100644 dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py create mode 100644 dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py diff --git a/dolphinscheduler-python/pydolphinscheduler/README.md b/dolphinscheduler-python/pydolphinscheduler/README.md index 9cc524d346..32a7ff6839 100644 --- a/dolphinscheduler-python/pydolphinscheduler/README.md +++ b/dolphinscheduler-python/pydolphinscheduler/README.md @@ -40,8 +40,8 @@ your workflow by python code, aka workflow-as-codes. # Install $ pip install apache-dolphinscheduler -# Check installation, it is success if you see version output, here we use 0.1.0 as example -$ python -c "import pydolphinscheduler; print(pydolphinscheduler.__version__)" +# Verify installation is successful, it will show the version of apache-dolphinscheduler, here we use 0.1.0 as example +$ pydolphinscheduler version 0.1.0 ``` diff --git a/dolphinscheduler-python/pydolphinscheduler/docs/source/cli.rst b/dolphinscheduler-python/pydolphinscheduler/docs/source/cli.rst new file mode 100644 index 0000000000..60e8231abf --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/docs/source/cli.rst @@ -0,0 +1,36 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + +.. http://www.apache.org/licenses/LICENSE-2.0 + +.. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +Command Line Interface +====================== + +*PyDolphinScheduler* have mechanism call CLI(command line interface) to help user control it in Shell. + +Prepare +------- + +You have to :ref:`install PyDolphinScheduler ` first before you using +its CLI + +Usage +----- + +Here is basic usage about the command line of *PyDolphinScheduler* + +.. click:: pydolphinscheduler.cli.commands:cli + :prog: pydolphinscheduler + :nested: full diff --git a/dolphinscheduler-python/pydolphinscheduler/docs/source/conf.py b/dolphinscheduler-python/pydolphinscheduler/docs/source/conf.py index 5ee73a5bb4..e22b3bb1b8 100644 --- a/dolphinscheduler-python/pydolphinscheduler/docs/source/conf.py +++ b/dolphinscheduler-python/pydolphinscheduler/docs/source/conf.py @@ -55,6 +55,8 @@ extensions = [ "sphinx.ext.viewcode", "sphinx.ext.autosectionlabel", "sphinx_rtd_theme", + # Documenting command line interface + "sphinx_click.ext", ] # Add any paths that contain templates here, relative to this directory. diff --git a/dolphinscheduler-python/pydolphinscheduler/docs/source/index.rst b/dolphinscheduler-python/pydolphinscheduler/docs/source/index.rst index b04c26f094..ad4c93d1f9 100644 --- a/dolphinscheduler-python/pydolphinscheduler/docs/source/index.rst +++ b/dolphinscheduler-python/pydolphinscheduler/docs/source/index.rst @@ -32,6 +32,7 @@ then go and see :doc:`tutorial` for more detail. tutorial concept tasks/index + cli api Indices and tables diff --git a/dolphinscheduler-python/pydolphinscheduler/setup.py b/dolphinscheduler-python/pydolphinscheduler/setup.py index cd6eb3eb77..117ffee76f 100644 --- a/dolphinscheduler-python/pydolphinscheduler/setup.py +++ b/dolphinscheduler-python/pydolphinscheduler/setup.py @@ -31,12 +31,14 @@ version = "0.1.0" # Start package required prod = [ + "click>=8.0.0", "py4j~=0.10", ] doc = [ "sphinx>=4.3", "sphinx_rtd_theme>=1.0", + "sphinx-click>=3.0", ] test = [ @@ -125,4 +127,9 @@ setup( "test": test, "doc": doc, }, + entry_points={ + "console_scripts": [ + "pydolphinscheduler = pydolphinscheduler.cli.commands:cli", + ], + }, ) diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/__init__.py new file mode 100644 index 0000000000..5f30c83241 --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/__init__.py @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Commands line interface of pydolphinscheduler.""" diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py new file mode 100644 index 0000000000..a430f457ad --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Commands line interface's command of pydolphinscheduler.""" + +import click +from click import echo + +from pydolphinscheduler import __version__ + +version_option_val = ["major", "minor", "micro"] + + +@click.group() +def cli(): + """Apache DolphinScheduler Python API's command line interface.""" + + +@cli.command() +@click.option( + "--part", + "-p", + required=False, + type=click.Choice(version_option_val, case_sensitive=False), + multiple=False, + help="The part of version your want to get.", +) +def version(part: str) -> None: + """Show current version of pydolphinscheduler.""" + if part: + idx = version_option_val.index(part) + echo(f"{__version__.split('.')[idx]}") + else: + echo(f"{__version__}") diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/cli/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/cli/__init__.py new file mode 100644 index 0000000000..f1a4396af6 --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/tests/cli/__init__.py @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Init command line interface tests.""" diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py new file mode 100644 index 0000000000..df17759ef3 --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Test command line interface subcommand version.""" + +import pytest + +from pydolphinscheduler import __version__ +from pydolphinscheduler.cli.commands import cli +from tests.testing.cli import CliTestWrapper + + +def test_version(): + """Test whether subcommand `version` correct.""" + cli_test = CliTestWrapper(cli, ["version"]) + cli_test.assert_success(output=f"{__version__}") + + +@pytest.mark.parametrize( + "part, idx", + [ + ("major", 0), + ("minor", 1), + ("micro", 2), + ], +) +def test_version_part(part: str, idx: int): + """Test subcommand `version` option `--part`.""" + cli_test = CliTestWrapper(cli, ["version", "--part", part]) + cli_test.assert_success(output=f"{__version__.split('.')[idx]}") + + +@pytest.mark.parametrize( + "option, output", + [ + # not support option + (["version", "--not-support"], "No such option"), + # not support option value + (["version", "--part", "abc"], "Invalid value for '--part'"), + ], +) +def test_version_not_support_option(option, output): + """Test subcommand `version` not support option or option value.""" + cli_test = CliTestWrapper(cli, option) + cli_test.assert_fail(ret_code=2, output=output, fuzzy=True) diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py b/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py new file mode 100644 index 0000000000..1585920183 --- /dev/null +++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Utils of command line test.""" + + +import os + +from click.testing import CliRunner + + +class CliTestWrapper: + """Wrap command click CliRunner.invoke.""" + + _dev_mode_env_name = "PY_DOLPHINSCHEDULER_DEV_MODE" + _dev_mode_true_val = {"true", "t", "1"} + + def __init__(self, *args, **kwargs): + runner = CliRunner() + self.result = runner.invoke(*args, **kwargs) + self.show_result_output() + + def _assert_output(self, output: str = None, fuzzy: bool = False): + """Assert between `CliRunner.invoke.result.output` and parameter `output`. + + :param output: The output will check compare to the ``CliRunner.invoke.output``. + :param fuzzy: A flag define whether assert :param:`output` in fuzzy or not. + Check if `CliRunner.invoke.output` contain :param:`output` is set ``True`` + and CliRunner.invoke.output equal to :param:`output` if we set it ``False``. + """ + if not output: + return + if fuzzy: + assert output in self.result.output + else: + assert self.result.output.rstrip("\n") == output + + def show_result_output(self): + """Print `CliRunner.invoke.result` output content in debug mode. + + It read variable named `PY_DOLPHINSCHEDULER_DEV_MODE` from env, when it set to `true` or `t` or `1` + will print result output when class :class:`CliTestWrapper` is initialization. + """ + dev_mode = str(os.getenv(self._dev_mode_env_name)) + if dev_mode.strip().lower() in self._dev_mode_true_val: + print(f"\n{self.result.output}\n") + + def assert_success(self, output: str = None, fuzzy: bool = False): + """Assert test is success. + + It would check whether `CliRunner.invoke.exit_code` equals to `0`, with no + exception at the same time. It's also can test the content of `CliRunner.invoke.output`. + + :param output: The output will check compare to the ``CliRunner.invoke.output``. + :param fuzzy: A flag define whether assert :param:`output` in fuzzy or not. + Check if `CliRunner.invoke.output` contain :param:`output` is set ``True`` + and CliRunner.invoke.output equal to :param:`output` if we set it ``False``. + """ + assert self.result.exit_code == 0 + if self.result.exception: + raise self.result.exception + self._assert_output(output, fuzzy) + + def assert_fail(self, ret_code: int, output: str = None, fuzzy: bool = False): + """Assert test is fail. + + It would check whether `CliRunner.invoke.exit_code` equals to :param:`ret_code`, + and it will also can test the content of `CliRunner.invoke.output`. + + :param ret_code: The returning code of this fail test. + :param output: The output will check compare to the ``CliRunner.invoke.output``. + :param fuzzy: A flag define whether assert :param:`output` in fuzzy or not. + Check if `CliRunner.invoke.output` contain :param:`output` is set ``True`` + and CliRunner.invoke.output equal to :param:`output` if we set it ``False``. + """ + assert ret_code == self.result.exit_code + self._assert_output(output, fuzzy)