Repository: opsdroid/opsdroid

Issue:
Switch CLI to use click
I recently discovered [`click`](http://click.pocoo.org/5/) which makes adding command line options and arguments to your python application super simple and much more robust.

We should look at replacing the [current argparse code](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/__main__.py#L88) with `click`!


Discussion:
I'm going to start looking at this.

Resolving patch:
diff --git a/opsdroid/__main__.py b/opsdroid/__main__.py
--- a/opsdroid/__main__.py
+++ b/opsdroid/__main__.py
@@ -3,12 +3,13 @@
 import os
 import sys
 import logging
-import argparse
 import gettext
 
+import click
+
 from opsdroid.core import OpsDroid
-from opsdroid.const import DEFAULT_LOG_FILENAME, EXAMPLE_CONFIG_FILE,\
-    DEFAULT_LANGUAGE, LOCALE_DIR
+from opsdroid.const import __version__, DEFAULT_LOG_FILENAME, \
+    EXAMPLE_CONFIG_FILE, DEFAULT_LANGUAGE, LOCALE_DIR
 from opsdroid.web import Web
 
 
@@ -85,14 +86,6 @@ def get_logging_level(logging_level):
     return logging.INFO
 
 
-def parse_args(args):
-    """Parse command line arguments."""
-    parser = argparse.ArgumentParser(description='Run opsdroid.')
-    parser.add_argument('--gen-config', action="store_true",
-                        help='prints out an example configuration file')
-    return parser.parse_args(args)
-
-
 def check_dependencies():
     """Check for system dependencies required by opsdroid."""
     if sys.version_info.major < 3 or sys.version_info.minor < 5:
@@ -100,6 +93,23 @@ def check_dependencies():
         sys.exit(1)
 
 
+def print_version(ctx, param, value):
+    """Print out the version of opsdroid that is installed."""
+    if not value or ctx.resilient_parsing:
+        return
+    click.echo('opsdroid v{version}'.format(version=__version__))
+    ctx.exit(0)
+
+
+def print_example_config(ctx, param, value):
+    """Print out the example config."""
+    if not value or ctx.resilient_parsing:
+        return
+    with open(EXAMPLE_CONFIG_FILE, 'r') as conf:
+        click.echo(conf.read())
+    ctx.exit(0)
+
+
 def welcome_message(config):
     """Add welcome message if set to true in configuration."""
     try:
@@ -120,15 +130,19 @@ def welcome_message(config):
                           "configuration.yaml"))
 
 
+@click.command()
+@click.option('--gen-config', is_flag=True, callback=print_example_config,
+              expose_value=False, default=False,
+              help='Print an example config and exit.')
+@click.option('--version', '-v', is_flag=True, callback=print_version,
+              expose_value=False, default=False, is_eager=True,
+              help='Print the version and exit.')
 def main():
-    """Parse the args and then start the application."""
-    args = parse_args(sys.argv[1:])
-
-    if args.gen_config:
-        with open(EXAMPLE_CONFIG_FILE, 'r') as conf:
-            print(conf.read())
-        sys.exit(0)
+    """Opsdroid is a chat bot framework written in Python.
 
+    It is designed to be extendable, scalable and simple.
+    See https://opsdroid.github.io/ for more information.
+    """
     check_dependencies()
 
     with OpsDroid() as opsdroid:


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,16 +1,19 @@
 
 import unittest
+import unittest.mock as mock
 import logging
 import os
 import sys
 import shutil
 import tempfile
-import unittest.mock as mock
 import gettext
 
+import click
+from click.testing import CliRunner
 
 import opsdroid.__main__ as opsdroid
 import opsdroid.web as web
+from opsdroid.const import __version__
 from opsdroid.core import OpsDroid
 from opsdroid.helper import del_rw
 
@@ -43,10 +46,6 @@ def test_init_doesnt_run(self):
                 opsdroid.init()
                 self.assertFalse(mainfunc.called)
 
-    def test_parse_args(self):
-        args = opsdroid.parse_args(["--gen-config"])
-        self.assertEqual(True, args.gen_config)
-
     def test_configure_no_lang(self):
         with mock.patch.object(gettext, "translation") as translation:
             opsdroid.configure_lang({})
@@ -166,9 +165,23 @@ def test_check_version_35(self):
                 self.fail("check_dependencies() exited unexpectedly!")
 
     def test_gen_config(self):
-        with mock.patch.object(sys, 'argv', ["opsdroid", "--gen-config"]):
-            with self.assertRaises(SystemExit):
-                opsdroid.main()
+        with mock.patch.object(click, 'echo') as click_echo,\
+                mock.patch('opsdroid.core.OpsDroid.load') as opsdroid_load:
+            runner = CliRunner()
+            result = runner.invoke(opsdroid.main, ['--gen-config'])
+            self.assertTrue(click_echo.called)
+            self.assertFalse(opsdroid_load.called)
+            self.assertEqual(result.exit_code, 0)
+
+    def test_print_version(self):
+        with mock.patch.object(click, 'echo') as click_echo,\
+                mock.patch('opsdroid.core.OpsDroid.load') as opsdroid_load:
+            runner = CliRunner()
+            result = runner.invoke(opsdroid.main, ['--version'])
+            self.assertTrue(click_echo.called)
+            self.assertFalse(opsdroid_load.called)
+            self.assertTrue(__version__ in click_echo.call_args[0][0])
+            self.assertEqual(result.exit_code, 0)
 
     def test_main(self):
         with mock.patch.object(sys, 'argv', ["opsdroid"]), \
@@ -178,7 +191,8 @@ def test_main(self):
                 mock.patch.object(OpsDroid, 'load') as mock_load, \
                 mock.patch.object(web, 'Web'), \
                 mock.patch.object(OpsDroid, 'start_loop') as mock_loop:
-            opsdroid.main()
+            runner = CliRunner()
+            runner.invoke(opsdroid.main, [])
             self.assertTrue(mock_cd.called)
             self.assertTrue(mock_cl.called)
             self.assertTrue(mock_wm.called)