Repository: markusressel/telegram-click

Issue:
Optional arguments
Although default values for arguments are already supported the current implementation does not allow the use of `None` as a default value.

It should be possible to use `None` as a default value by simply making an argument optional. The default default value will then be `None` and can be changed by the dev.

Discussion:

Patch:
diff --git a/example.py b/example.py
index 0e22e94..44b4dbc 100644
--- a/example.py
+++ b/example.py
@@ -43,11 +43,13 @@ class MyPermission(Permission):
 class MyBot:
 
     def __init__(self):
-        self._updater = Updater(token=os.environ.get("TELEGRAM_BOT_KEY"),
-                                use_context=True)
+        self._updater = Updater(
+            token=os.environ.get("TELEGRAM_BOT_KEY"),
+            use_context=True
+        )
 
         handler_groups = {
-            1: [MessageHandler(Filters.text, callback=self._message_callback),
+            1: [
                 CommandHandler('commands',
                                filters=(~ Filters.forwarded) & (~ Filters.reply),
                                callback=self._commands_command_callback),
@@ -65,7 +67,7 @@ class MyBot:
                                callback=self._children_command_callback),
                 # Unknown command handler
                 MessageHandler(Filters.command, callback=self._unknown_command_callback)
-                ]
+            ]
         }
 
         for group, handlers in handler_groups.items():
@@ -80,11 +82,6 @@ class MyBot:
         self._updater.start_polling(clean=True)
         self._updater.idle()
 
-    def _message_callback(self, update: Update, context: CallbackContext):
-        text = update.effective_message.text
-        print("Message: {}".format(text))
-        pass
-
     # optionally specify this callback to send a list of all available commands if
     # an unsupported command is used
     @command(name="commands",
@@ -113,15 +110,21 @@ class MyBot:
         bot.send_message(chat_id, text, parse_mode=ParseMode.MARKDOWN)
 
     @command(name='name',
-             description='Set a name',
+             description='Get/Set a name',
              arguments=[
                  Argument(name='name',
                           description='The new name',
                           validator=lambda x: x.strip(),
+                          optional=True,
                           example='Markus')
              ])
     def _name_command_callback(self, update: Update, context: CallbackContext, name: str):
-        context.bot.send_message(update.effective_chat.id, "New name: {}".format(name))
+        chat_id = update.effective_chat.id
+        if name is None:
+            context.bot.send_message(chat_id, "Current name: {}".format(name))
+            return
+        else:
+            context.bot.send_message(chat_id, "New name: {}".format(name))
 
     @command(name='age',
              description='Set age',
diff --git a/telegram_click/argument.py b/telegram_click/argument.py
index 0b033b8..9e9c26c 100644
--- a/telegram_click/argument.py
+++ b/telegram_click/argument.py
@@ -32,7 +32,7 @@ class Argument:
     """
 
     def __init__(self, name: str, description: str, example: str, type: type = str, converter: callable = None,
-                 default: any = None, validator: callable = None):
+                 optional: bool = True, default: any = None, validator: callable = None):
         """
         Creates a command argument object
         :param name: the name of the argument
@@ -40,6 +40,7 @@ class Argument:
         :param example: an example (string!) value for this argument
         :param type: the expected type of the argument
         :param converter: a converter function to convert the string value to the expected type
+        :param optional: specifies if this argument is optional
         :param default: an optional default value
         :param validator: a validator function
         """
@@ -63,17 +64,18 @@ class Argument:
                 raise ValueError("If you want to use a custom type you have to provide a converter function too!")
         else:
             self.converter = converter
+        self.optional = optional
         self.default = default
         self.validator = validator
 
-    def parse_arg(self, arg: str) -> any:
+    def parse_arg_value(self, arg: str) -> any:
         """
         Tries to parse the given value
         :param arg: the string value
         :return: the parsed value
         """
         if arg is None:
-            if self.default is not None:
+            if self.optional:
                 return self.default
             else:
                 raise ValueError("Missing argument: {}".format(self.name))
@@ -94,8 +96,8 @@ class Argument:
             self.type.__name__,
             escape_for_markdown(self.description)
         )
-        if self.default is not None:
-            message += " (default: {}".format(escape_for_markdown(self.default))
+        if self.optional:
+            message += " (`{}`)".format(escape_for_markdown(self.default))
         return message
 
     @staticmethod
@@ -132,7 +134,7 @@ class Selection(Argument):
     """
 
     def __init__(self, name: str, description: str, allowed_values: [any], type: type = str, converter: callable = None,
-                 default: any = None):
+                 optional: bool = None, default: any = None):
         """
 
         :param name: the name of the argument
@@ -140,18 +142,12 @@ class Selection(Argument):
         :param allowed_values: list of allowed (target type) values
         :param type: the expected type of the argument
         :param converter: a converter function to convert the string value to the expected type
+        :param optional: specifies if this argument is optional
         :param default: an optional default value
         """
         self.allowed_values = allowed_values
-        validator = lambda x: x in self.allowed_values
-        super().__init__(name, description, allowed_values[0], type, converter, default, validator)
 
-    def generate_argument_message(self):
-        message = "  {} (`{}`): {}".format(
-            escape_for_markdown(self.name),
-            self.type.__name__,
-            escape_for_markdown(self.description)
-        )
-        if self.default is not None:
-            message += " (default: {}".format(escape_for_markdown(self.default))
-        return message
+        def validator(x):
+            return x in self.allowed_values
+
+        super().__init__(name, description, allowed_values[0], type, converter, optional, default, validator)
diff --git a/telegram_click/util.py b/telegram_click/util.py
index 3849279..10d951d 100644
--- a/telegram_click/util.py
+++ b/telegram_click/util.py
@@ -39,12 +39,13 @@ def find_first(args: [], type: type):
             return arg
 
 
-def escape_for_markdown(text: str) -> str:
+def escape_for_markdown(text: str or None) -> str:
     """
     Escapes text to use as plain text in a markdown document
     :param text: the original text
     :return: the escaped text
     """
+    text = str(text)
     escaped = text.replace("*", "\\*").replace("_", "\\_")
     return escaped
 
@@ -90,7 +91,7 @@ def parse_command_args(arguments: str, expected_args: []) -> dict:
     # process named args first
     for name, value in named:
         if name in arg_name_map:
-            parsed_args[name] = arg_name_map[name].parse_arg(value)
+            parsed_args[name] = arg_name_map[name].parse_arg_value(value)
             arg_name_map.pop(name)
         else:
             raise ValueError("Unknown argument '{}'".format(name))
@@ -98,12 +99,12 @@ def parse_command_args(arguments: str, expected_args: []) -> dict:
     # then floating args
     for floating_arg in floating:
         arg = list(arg_name_map.values())[0]
-        parsed_args[arg.name] = arg.parse_arg(floating_arg)
+        parsed_args[arg.name] = arg.parse_arg_value(floating_arg)
         arg_name_map.pop(arg.name)
 
     # and then handle missing args
     for name, arg in arg_name_map.items():
-        parsed_args[arg.name] = arg.parse_arg(None)
+        parsed_args[arg.name] = arg.parse_arg_value(None)
 
     return parsed_args
 


Test patch:
diff --git a/tests/argument_test.py b/tests/argument_test.py
index c7c0e60..851bbb9 100644
--- a/tests/argument_test.py
+++ b/tests/argument_test.py
@@ -32,7 +32,7 @@ class ArgumentTest(TestBase):
             example="text"
         )
 
-        assert arg.parse_arg("sample") == "sample"
+        assert arg.parse_arg_value("sample") == "sample"
 
     @staticmethod
     def test_bool_argument():
@@ -43,7 +43,7 @@ class ArgumentTest(TestBase):
             example="0"
         )
 
-        assert not arg.parse_arg("0")
+        assert not arg.parse_arg_value("0")
 
     @staticmethod
     def test_int_argument():
@@ -54,9 +54,9 @@ class ArgumentTest(TestBase):
             example="0"
         )
 
-        assert arg.parse_arg("0") == 0
-        assert arg.parse_arg("01") == 1
-        assert arg.parse_arg("10") == 10
+        assert arg.parse_arg_value("0") == 0
+        assert arg.parse_arg_value("01") == 1
+        assert arg.parse_arg_value("10") == 10
 
     @staticmethod
     def test_float_argument():
@@ -67,8 +67,8 @@ class ArgumentTest(TestBase):
             example="1.2"
         )
 
-        assert arg.parse_arg("0") == 0.0
-        assert arg.parse_arg("01") == 1.0
-        assert arg.parse_arg("10") == 10.0
-        assert arg.parse_arg("10.2") == 10.2
-        assert arg.parse_arg("3%") == 0.03
+        assert arg.parse_arg_value("0") == 0.0
+        assert arg.parse_arg_value("01") == 1.0
+        assert arg.parse_arg_value("10") == 10.0
+        assert arg.parse_arg_value("10.2") == 10.2
+        assert arg.parse_arg_value("3%") == 0.03