Repository: neuralmagic/sparsify (language: python)

Issue:
[bugfix] make sparsify.beta tests pass
This PR accomplishes the following:
- Switches on tests for sparsify.beta in GHA workflow
- Cherry picks changes from #216 to include login in sparsify.beta
- fixes a renaming issue where some instances of the old function name were not updated
- At some point the TaskName class was update to accept things like `image classification` (with a space in b/w) as a valid TaskName but the tests were not, this PR updates the tests accordingly
- Updates version for sparsify.beta from diff #219 
- Install sparseml[yolov5] before running auto tests


## PYDANTIC CLI REFACTOR
This branch also contains commits for the pydantic CLI refactor see PRs #209 and #221 


Discussion / hints:

Fix patch:
diff --git a/src/sparsify/cli/utils/__init__.py b/src/sparsify/cli/utils/__init__.py
new file mode 100644
index 00000000..8f53c02b
--- /dev/null
+++ b/src/sparsify/cli/utils/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved.
+#
+# 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.
+
+# flake8: noqa
+# isort: skip_file
+
+from .helpers import *
diff --git a/src/sparsify/cli/utils/helpers.py b/src/sparsify/cli/utils/helpers.py
new file mode 100644
index 00000000..9150a163
--- /dev/null
+++ b/src/sparsify/cli/utils/helpers.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved.
+#
+# 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 typing
+from enum import Enum
+
+import click
+from pydantic import BaseModel
+
+
+__all__ = [
+    "get_click_options_from_base_models",
+    "get_base_models_from_options",
+]
+
+
+def get_click_options_from_base_models(
+    base_models: typing.Union[BaseModel, typing.List[BaseModel]]
+):
+    """
+    A decorator that takes pydantic BaseModels and converts them into a click options
+
+    :param base_models: a pydantic BaseModel or list of BaseModels to convert
+        into click options
+    :return: a decorated function that adds click options to the click command it
+        is applied to
+    """
+
+    def wrapper(func):
+        """
+        :param func: the function to add the click options to, this function should
+            also be a click command, (i.e decorated with @click.command or @click.group)
+        """
+        options = []
+        _base_models = base_models if isinstance(base_models, list) else [base_models]
+        for base_model in _base_models:
+            for field in base_model.__fields__.values():
+                args = ["--{}".format(field.name)]
+                kwargs = {}
+
+                kwargs["type"] = _get_type_from_outer_type(field.outer_type_)
+                if field.allow_none:
+                    kwargs["default"] = None
+
+                if not field.required:
+                    kwargs["default"] = field.get_default()
+                else:
+                    kwargs["required"] = field.required
+
+                kwargs["help"] = field.field_info.description or ""
+                options.append(click.option(*args, **kwargs))
+
+        for option in options:
+            func = option(func)
+        return func
+
+    return wrapper
+
+
+def get_base_models_from_options(
+    models: typing.Union[BaseModel, typing.List[BaseModel]],
+    options: typing.Dict[str, typing.Any],
+) -> typing.List[BaseModel]:
+    if not isinstance(models, list):
+        models = [models]
+
+    model_objects = [model.parse_obj(options) for model in models]
+
+    return model_objects
+
+
+def _get_type_from_outer_type(outer_type):
+    if isinstance(outer_type, Enum):
+        return click.Choice([e.value for e in outer_type], case_sensitive=False)
+
+    if typing.get_origin(outer_type) is typing.Union:
+        raise ValueError("Union types are not supported")
+
+    return outer_type


Test patch:
diff --git a/tests/sparsify/cli/test_helpers.py b/tests/sparsify/cli/test_helpers.py
new file mode 100644
index 00000000..4cc62241
--- /dev/null
+++ b/tests/sparsify/cli/test_helpers.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved.
+#
+# 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 enum import Enum
+from typing import Optional
+
+import pytest
+
+import click
+from click.testing import CliRunner
+from pydantic import BaseModel, Field, ValidationError
+from sparsify.cli.utils.helpers import (
+    get_base_models_from_options,
+    get_click_options_from_base_models,
+)
+
+
+class SimpleFieldsWithDefaults(BaseModel):
+    a: int = Field(default=1, description="A simple integer")
+    b: int = Field(default=2, description="Another simple integer")
+
+
+class SimpleFieldWithDefaultFactory(BaseModel):
+    a: int = Field(default_factory=lambda: 1, description="A simple integer")
+
+
+class SimpleFieldsWithoutDefaults(BaseModel):
+    a: int = Field(description="A simple required integer")
+    b: int = Field(description="Another simple required integer")
+
+
+class OptionalStringField(BaseModel):
+    a: Optional[str] = Field(description="An optional string")
+
+
+class FooEnum(Enum):
+    FOO = "foo"
+    BAR = "bar"
+
+
+class SimpleFieldsWithEnum(BaseModel):
+    a: FooEnum = Field(description="A simple enum")
+
+
+class MutuallyExclusiveFieldsA(BaseModel):
+    a: int = Field(description="A simple integer")
+
+
+class MutuallyExclusiveFieldsB(BaseModel):
+    b: int = Field(description="Another simple integer")
+
+
+@pytest.mark.parametrize(
+    "model_or_models, args, exit_code",
+    [
+        (SimpleFieldsWithDefaults, ["--a", "3", "--b", "4"], 0),
+        (SimpleFieldsWithDefaults, ["--a", "3.14", "--b", "4"], 2),
+        (SimpleFieldsWithDefaults, [], 0),
+        (SimpleFieldsWithDefaults, ["--help"], 0),
+        (SimpleFieldsWithoutDefaults, ["--help"], 0),
+        (SimpleFieldsWithoutDefaults, [], 2),
+        (SimpleFieldsWithoutDefaults, ["--a", "1"], 2),
+        (SimpleFieldWithDefaultFactory, [], 0),
+        (SimpleFieldWithDefaultFactory, ["--a", "3"], 0),
+        (OptionalStringField, [], 0),
+        (OptionalStringField, ["--a", "foo"], 0),
+        (SimpleFieldsWithEnum, ["--a", "foo"], 0),
+        (SimpleFieldsWithEnum, ["--a", "blah"], 2),
+        (
+            [MutuallyExclusiveFieldsA, MutuallyExclusiveFieldsB],
+            ["--a", "1", "--b", "2"],
+            0,
+        ),
+        ([MutuallyExclusiveFieldsA, MutuallyExclusiveFieldsB], ["--a", "1"], 2),
+        ([MutuallyExclusiveFieldsA, MutuallyExclusiveFieldsB], ["--help"], 0),
+    ],
+)
+def test_simple_fields(model_or_models, args, exit_code):
+    @click.command()
+    @get_click_options_from_base_models(model_or_models)
+    def main(**kwargs):
+        pass
+
+    runner = CliRunner()
+    result = runner.invoke(main, args)
+    assert result.exit_code == exit_code
+
+
+@pytest.mark.parametrize(
+    "model_or_models, args, expected_error",
+    [
+        (SimpleFieldsWithDefaults, {"a": 3, "b": 4}, None),
+        (SimpleFieldsWithDefaults, {"a": 3.14, "b": 4}, None),  # will be cast to int
+        (SimpleFieldsWithDefaults, {"a": 3.14, "b": "blah"}, ValidationError),
+        (OptionalStringField, {}, None),
+    ],
+)
+def test_get_base_models_from_options(model_or_models, args, expected_error):
+    if expected_error:
+        with pytest.raises(expected_error):
+            get_base_models_from_options(model_or_models, args)
+
+    else:
+        base_models = get_base_models_from_options(model_or_models, args)
+
+        models = model_or_models
+        if not isinstance(models, list):
+            models = [models]
+
+        assert len(base_models) == len(models)
+
+        for expected_model_class, actual_model in zip(models, base_models):
+            assert expected_model_class == actual_model.__class__
+            for field in expected_model_class.__fields__.values():
+                if field.name in args:
+                    actual_field_value = getattr(actual_model, field.name)
+                    expected_field_value = args[field.name]
+                    assert (
+                        actual_field_value == expected_field_value
+                        or actual_field_value == field.outer_type_(expected_field_value)
+                    )