diff --git a/CHANGELOG.md b/CHANGELOG.md index d11ada14..2062c342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,17 @@ Release date: UNRELEASED * Added HPGL configuration for the Calcomp Artisan plotter (thanks to Andee Collard and @ithinkido) (#418) + ### Bug fixes * Fixed issue with `write` where layer opacity was included in the `stroke` attribute instead of using `stroke-opacity`, which, although compliant, was not compatible with Inkscape (#429) + +### API changes + +* Added `vpype_cli.FloatType()`, `vpype_cli.IntRangeType()`, and `vpype_cli.ChoiceType()` (#430) + + ### Other changes * Added support for Python 3.10 and dropped support for Python 3.7 (#417) diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 00000000..3636ce24 --- /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") diff --git a/vpype_cli/state.py b/vpype_cli/state.py index bb26fe08..f9c04e27 100644 --- 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 index 6664be62..80520815 100644 --- 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 ` 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.