Repository: pallets/click

Issue:
Unprintable MissingParameter exception
My code is (correctly) hitting https://github.com/pallets/click/blob/master/click/core.py#L1444 and raising a MissingParameter exception. However, there's no `message` passed there, which means `exc.message` is defaulted to `None` by the base class (`ClickException`) constructor here https://github.com/pallets/click/blob/master/click/exceptions.py#L23.

With other `ClickException`s I've seen (e.g. `NoSuchOption` https://github.com/pallets/click/blob/master/click/exceptions.py#L165), if message is set to None then it gets reset as something more sensible e.g. `"No such option 'foo'"`. This means that doing `str(exc)` works nicely. However, `MissingParameter` doesn't have this, which means any attempt to stringify (e.g. print or log) a `MissingParameter` actually causes a `TypeError: __str__ returned non-string (type NoneType)` and then things go badly wrong.

Is it expected that `MissingParameter` is an unprintable exception when caused e.g. by missing out a required argument? It seems odd that no one else has come across this before. I'm using click in a slightly unusual context in that I'm using the group/command/arg/option parsing stuff outside of a traditional command line script, which might be why no one has seen this if click does it's own special handling for `MissingParameter` errors.

TL;DR It would be nice if `MissingParameter` was printable when raised via `Parameter.full_process_value` (or ideally more generally).

It might be enough to have something along the lines of what `NoSuchOption` does?


Discussion:
Hello. Any progress on this?
The PR above does not pass the tests. I can't investigate further at the moment.
This is still a problem.  I just hit this myself in click 7.0.0
I'll take a look at this.

Resolving patch:
diff --git a/click/exceptions.py b/click/exceptions.py
--- a/click/exceptions.py
+++ b/click/exceptions.py
@@ -152,6 +152,19 @@ def format_message(self):
             msg or '',
         )
 
+    def __str__(self):
+        if self.message is None:
+            param_name = self.param.name if self.param else None
+            return "missing parameter: {}".format(param_name)
+        else:
+            return self.message
+
+    if PY2:
+        __unicode__ = __str__
+
+        def __str__(self):
+            return self.__unicode__().encode("utf-8")
+
 
 class NoSuchOption(UsageError):
     """Raised if click attempted to handle an option that does not


Test patch:
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
--- a/tests/test_arguments.py
+++ b/tests/test_arguments.py
@@ -203,6 +203,15 @@ def cmd(arg):
     assert 'Missing argument "ARG".' in result.output
 
 
+def test_missing_argument_string_cast():
+    ctx = click.Context(click.Command(""))
+
+    with pytest.raises(click.MissingParameter) as excinfo:
+        click.Argument(["a"], required=True).full_process_value(ctx, None)
+
+    assert str(excinfo.value) == "missing parameter: a"
+
+
 def test_implicit_non_required(runner):
     @click.command()
     @click.argument('f', default='test')
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -321,6 +321,15 @@ def cmd(whatever):
     assert result.output == '23\n'
 
 
+def test_missing_option_string_cast():
+    ctx = click.Context(click.Command(""))
+
+    with pytest.raises(click.MissingParameter) as excinfo:
+        click.Option(['-a'], required=True).full_process_value(ctx, None)
+
+    assert str(excinfo.value) == "missing parameter: a"
+
+
 def test_missing_choice(runner):
     @click.command()
     @click.option('--foo', type=click.Choice(['foo', 'bar']),