Repository: abey79/vpype

Issue:
vpype_cli: add FloatType, IntRangeType, ChoiceType
This would be useful for plug-ins (e.g. `flow_img`)


Discussion:

Resolving patch:
diff --git a/vpype_cli/state.py b/vpype_cli/state.py
--- a/vpype_cli/state.py
+++ b/vpype_cli/state.py
@@ -89,7 +89,10 @@ def preprocess_argument(self, arg: Any) -> Any:
         if isinstance(arg, tuple):
             return tuple(self.preprocess_argument(item) for item in arg)
         else:
-            return arg.evaluate(self) if isinstance(arg, _DeferredEvaluator) else arg
+            try:
+                return arg.evaluate(self) if isinstance(arg, _DeferredEvaluator) else arg
+            except Exception as exc:
+                raise click.BadParameter(str(exc)) from exc
 
     def preprocess_arguments(
         self, args: tuple[Any, ...], kwargs: dict[str, Any]
diff --git a/vpype_cli/types.py b/vpype_cli/types.py
--- a/vpype_cli/types.py
+++ b/vpype_cli/types.py
@@ -79,6 +79,30 @@ def evaluate(self, state: State) -> int:
     _evaluator_class = _IntegerDeferredEvaluator
 
 
+class FloatType(_DeferredEvaluatorType):
+    """:class:`click.ParamType` sub-class to automatically perform
+    :ref:`property substitution <fundamentals_property_substitution>` on user input.
+
+    Example::
+
+        >>> import click
+        >>> import vpype_cli
+        >>> import vpype
+        >>> @vpype_cli.cli.command(group="my commands")
+        ... @click.argument("number", type=vpype_cli.FloatType())
+        ... @vpype_cli.generator
+        ... def my_command(number: float):
+        ...     pass
+    """
+
+    class _FloatDeferredEvaluator(_DeferredEvaluator):
+        def evaluate(self, state: State) -> float:
+            return float(state.substitute(self._text))
+
+    name = "number"
+    _evaluator_class = _FloatDeferredEvaluator
+
+
 class LengthType(_DeferredEvaluatorType):
     """:class:`click.ParamType` sub-class to automatically converts a user-provided lengths
     into CSS pixel units.
@@ -215,6 +239,18 @@ class FileType(_DelegatedDeferredEvaluatorType):
     _delegate_class = click.File
 
 
+class IntRangeType(_DelegatedDeferredEvaluatorType):
+    """:class:`click.File` clone which performs substitution on input."""
+
+    name = "float"
+    _delegate_class = click.IntRange
+
+
+class ChoiceType(_DelegatedDeferredEvaluatorType):
+    name = "choice"
+    _delegate_class = click.Choice
+
+
 def multiple_to_layer_ids(layers: int | list[int] | None, document: vp.Document) -> list[int]:
     """Convert multiple-layer CLI argument to list of layer IDs.
 


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
new file mode 100644
--- /dev/null
+++ b/tests/test_types.py
@@ -0,0 +1,49 @@
+import click
+import pytest
+
+import vpype as vp
+import vpype_cli
+
+
+def test_float_type():
+    @vpype_cli.cli.command(name="floatcmd")
+    @click.argument("arg", type=vpype_cli.FloatType())
+    @vpype_cli.generator
+    def cmd(arg: float) -> vp.LineCollection:
+        assert type(arg) is float
+        return vp.LineCollection()
+
+    vpype_cli.execute("floatcmd 3.5")
+    with pytest.raises(click.BadParameter):
+        vpype_cli.execute("floatcmd hello")
+
+
+def test_int_range_type():
+    @vpype_cli.cli.command(name="intrangecmd")
+    @click.argument("arg", type=vpype_cli.IntRangeType(min=10, max=14))
+    @vpype_cli.generator
+    def cmd(arg: int) -> vp.LineCollection:
+        assert type(arg) is int
+        assert 10 <= arg <= 14
+        return vp.LineCollection()
+
+    vpype_cli.execute("intrangecmd 10")
+    vpype_cli.execute("intrangecmd 14")
+    with pytest.raises(click.BadParameter):
+        vpype_cli.execute("intrangecmd 12.5")
+    with pytest.raises(click.BadParameter):
+        vpype_cli.execute("intrangecmd 9")
+
+
+def test_choice_type():
+    @vpype_cli.cli.command(name="choicecmd")
+    @click.argument("arg", type=vpype_cli.ChoiceType(choices=["yes", "no"]))
+    @vpype_cli.generator
+    def cmd(arg: str) -> vp.LineCollection:
+        assert arg in ["yes", "no"]
+        return vp.LineCollection()
+
+    vpype_cli.execute("choicecmd yes")
+    vpype_cli.execute("choicecmd no")
+    with pytest.raises(click.BadParameter):
+        vpype_cli.execute("choicecmd maybe")