Skip to content

Commit

Permalink
allow both singular and plural versions of nouns
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-mccarthy committed Apr 18, 2022
1 parent 69f459b commit 082d15f
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 16 deletions.
9 changes: 1 addition & 8 deletions sdk/python/kfp/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,11 @@
import sys

import click
from kfp.cli import (cli, components, diagnose_me_cli, experiment, pipeline,
recurring_run, run)
from kfp.cli import cli


def main():
logging.basicConfig(format='%(message)s', level=logging.INFO)
cli.cli.add_command(run.run)
cli.cli.add_command(recurring_run.recurring_run)
cli.cli.add_command(pipeline.pipeline)
cli.cli.add_command(diagnose_me_cli.diagnose_me)
cli.cli.add_command(experiment.experiment)
cli.cli.add_command(components.components)
try:
cli.cli(obj={}, auto_envvar_prefix='KFP')
except Exception as e:
Expand Down
16 changes: 14 additions & 2 deletions sdk/python/kfp/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2018 The Kubeflow Authors
# Copyright 2018-2022 The Kubeflow Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -13,11 +13,23 @@
# limitations under the License.

import click
from kfp.cli import component
from kfp.cli import diagnose_me_cli
from kfp.cli import experiment
from kfp.cli import pipeline
from kfp.cli import recurring_run
from kfp.cli import run
from kfp.cli.output import OutputFormat
from kfp.cli.utils import aliased_plurals_group
from kfp.client import Client

COMMANDS = [
run.run, recurring_run.recurring_run, experiment.experiment,
pipeline.pipeline, diagnose_me_cli.diagnose_me, component.component
]

@click.group()

@click.group(cls=aliased_plurals_group.AliasedPluralsGroup, commands=COMMANDS)
@click.option('--endpoint', help='Endpoint of the KFP API service to connect.')
@click.option('--iap-client-id', help='Client ID for IAP protected endpoint.')
@click.option(
Expand Down
50 changes: 50 additions & 0 deletions sdk/python/kfp/cli/cli_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2022 The Kubeflow Authors
#
# Licensed 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.

import functools
import sys
import unittest
from unittest import mock

from click import testing

# Docker is an optional install, but we need the import to succeed for tests.
# So we patch it before importing kfp.cli.components.
try:
import docker
except ImportError:
sys.modules['docker'] = mock.Mock()

from kfp.cli import cli


class TestCli(unittest.TestCase):

def setUp(self):
runner = testing.CliRunner()
self.invoke = functools.partial(
runner.invoke, cli=cli.cli, catch_exceptions=False, obj={})

def test_aliases_singular(self):
result = self.invoke(args=['component'])
self.assertEqual(result.exit_code, 0)

def test_aliases_plural(self):
result = self.invoke(args=['runs'])
self.assertEqual(result.exit_code, 0)

def test_aliases_fails(self):
result = self.invoke(args=['runss'])
self.assertEqual(result.exit_code, 2)
self.assertEqual("Error: Unrecognized command 'runss'\n", result.output)
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
_DOCKER_IS_PRESENT = False

import kfp as kfp
from kfp.components import component_factory, kfp_config, utils
from kfp.components import component_factory
from kfp.components import kfp_config
from kfp.components import utils

_REQUIREMENTS_TXT = 'requirements.txt'

Expand Down Expand Up @@ -329,12 +331,12 @@ def build_image(self, push_image: bool = True):


@click.group()
def components():
def component():
"""Builds shareable, containerized components."""
pass


@components.command()
@component.command()
@click.argument(
"components_directory",
type=pathlib.Path,
Expand Down
11 changes: 8 additions & 3 deletions sdk/python/kfp/cli/components_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import docker # pylint: disable=unused-import
except ImportError:
sys.modules['docker'] = mock.Mock()
from kfp.cli import components
from kfp.cli import component


def _make_component(func_name: str,
Expand Down Expand Up @@ -69,8 +69,8 @@ class Test(unittest.TestCase):

def setUp(self) -> None:
self.runner = testing.CliRunner()
self.cli = components.components
components._DOCKER_IS_PRESENT = True
self.cli = component.component
component._DOCKER_IS_PRESENT = True

patcher = mock.patch('docker.from_env')
self._docker_client = patcher.start().return_value
Expand Down Expand Up @@ -247,6 +247,11 @@ def testComponentFilepatternCanBeUsedToRestrictDiscovery(self):
str(self._working_dir), '--component-filepattern=train/*'
],
)
print("OUTPUT", result.output)
print("DONE")
print(result.exc_info)
import traceback
traceback.print_tb(result.exc_info[2])
self.assertEqual(result.exit_code, 0)

self.assertFileExistsAndContains(
Expand Down
13 changes: 13 additions & 0 deletions sdk/python/kfp/cli/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2022 The Kubeflow Authors
#
# Licensed 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.
37 changes: 37 additions & 0 deletions sdk/python/kfp/cli/utils/aliased_plurals_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2022 The Kubeflow Authors
#
# Licensed 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.

from typing import List, Tuple, Union

import click


class AliasedPluralsGroup(click.Group):

def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command:
regular = click.Group.get_command(self, ctx, cmd_name)
if regular is not None:
return regular
elif cmd_name.endswith("s"):
singular = click.Group.get_command(self, ctx, cmd_name[:-1])
if singular is not None:
return singular
raise click.UsageError(f"Unrecognized command '{cmd_name}'")

def resolve_command(
self, ctx: click.Context, args: List[str]
) -> Tuple[Union[str, None], Union[click.Command, None], List[str]]:
# always return the full command name
_, cmd, args = super().resolve_command(ctx, args)
return cmd.name, cmd, args # type: ignore
55 changes: 55 additions & 0 deletions sdk/python/kfp/cli/utils/aliased_plurals_group_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2022 The Kubeflow Authors
#
# Licensed 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.

import unittest

import click
from click import testing
from kfp.cli.utils import aliased_plurals_group


@click.group(cls=aliased_plurals_group.AliasedPluralsGroup)
def cli():
pass


@cli.command()
def command():
click.echo('Called command.')


class TestAliasedPluralsGroup(unittest.TestCase):

def setUp(self):
self.runner = testing.CliRunner()

def test_aliases_default_success(self):
result = self.runner.invoke(cli, ['command'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.output, "Called command.\n")

def test_aliases_plural_success(self):
result = self.runner.invoke(cli, ['commands'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.output, "Called command.\n")

def test_aliases_failure(self):
result = self.runner.invoke(cli, ['commandss'])
self.assertEqual(result.exit_code, 2)
self.assertEqual("Error: Unrecognized command 'commandss'\n",
result.output)


if __name__ == "__main__":
unittest.main()

0 comments on commit 082d15f

Please sign in to comment.