Repository: mbiokyle29/place

Issue:
build out logging
Place currently does not have any logging setup. A root logger should be configured in cli:main and provisions should be made for other modules to create logger instances (just make sure this works). By default only `error` should be logged. A command line argument should be added `-v`/`--verbose` which should enabled  `warn` and `info` level logging, and a `--debug` argument for `debug` level logging. The levels should be configured on the root logger such that all other loggers inherit.

AC
- root logger configured in cli:main with default level `error`, and stream handler and a sane log line config
- `-v/--verbose` and `--debug` command line options are added
- `-v/--verbose` command line option sets log level to info (including warning)
- `--debug` command line option sets log level to debug
- Other modules (example `lib/utils.py`) can register loggers which inherit the log level and format from the root logger. Prove this works with tests, you can use the cli.Runner result like here: https://github.com/mbiokyle29/place/blob/master/tests/test_main.py#L53. It has an `.output` attr with the output of the call

Comments:

Patch:
diff --git a/place/lib/utils.py b/place/lib/utils.py
index f7e3733..cfb3da5 100644
--- a/place/lib/utils.py
+++ b/place/lib/utils.py
@@ -1,9 +1,25 @@
 # file: utils.py
 # author: mbiokyle29
+import logging
 import os.path as path
 
 
+logger = logging.getLogger(__name__)
+
+
 def is_dir(input_path):
     """ Return a boolean if the path is not a dir """
     abs_path = path.abspath(input_path)
+    logger.debug("Checking if path: %s is a directory", abs_path)
     return path.isdir(abs_path)
+
+
+def configure_logger(logger, level):
+    logger.setLevel(level)
+    sh = logging.StreamHandler()
+    sh.setLevel(level)
+    formatter = logging.Formatter("[%(name)s][%(levelname)s]: %(message)s")
+    sh.setFormatter(formatter)
+    logger.addHandler(sh)
+
+    return logger
diff --git a/place/main.py b/place/main.py
index 1e99b58..4c93112 100644
--- a/place/main.py
+++ b/place/main.py
@@ -1,21 +1,31 @@
 # file: main.py
 # author: mbiokyle29
+import logging
 import os
 import sys
 
-from click import argument, command, Path
+from click import argument, command, option, Path
 
-from place.lib.utils import is_dir
+from place.lib.utils import is_dir, configure_logger
+
+
+root_logger = logging.getLogger("")
 
 
 @command()
 @argument("sources", nargs=-1, type=Path(exists=True, resolve_path=True))
 @argument("target", nargs=1, type=Path())
-def cli(sources, target):
+@option("-v", "--verbose", default=False, is_flag=True, help="Enable verbose logging.")
+@option("-d", "--debug", default=False, is_flag=True, help="Enable debug logging.")
+def cli(sources, target, verbose, debug):
     """ mv file(s) in SOURCES to TARGET while updating config files """
+    log_level = logging.INFO if verbose else logging.WARN
+    log_level = logging.DEBUG if debug else log_level
+    configure_logger(root_logger, log_level)
 
     # rename - 1 source, target must not be a dir
     if len(sources) == 1 and not is_dir(target):
+        root_logger.debug("Renaming source file %s to target %s", sources[0], target)
         os.rename(sources[0], target)
 
     # move - n sources, target must be a dir
@@ -25,8 +35,10 @@ def cli(sources, target):
                 os.path.abspath(target),
                 os.path.basename(source)
             )
+            root_logger.debug("Moving source file %s to target location %s", source, target)
             os.rename(source, new_path)
 
     # badness
     else:
+        root_logger.warning("No placeable files detected in input. Quitting...")
         sys.exit(-1)


Test patch:
diff --git a/tests/lib/test_utils.py b/tests/lib/test_utils.py
index 22039bf..0e16a93 100644
--- a/tests/lib/test_utils.py
+++ b/tests/lib/test_utils.py
@@ -1,11 +1,13 @@
 # file: test_utils.py
 # author: mbiokyle29
+import logging
 import os
 import unittest
 from shutil import rmtree
 from tempfile import mkdtemp, NamedTemporaryFile
+from unittest.mock import MagicMock
 
-from place.lib.utils import is_dir
+from place.lib.utils import is_dir, configure_logger
 
 
 class TestIsDir(unittest.TestCase):
@@ -33,3 +35,16 @@ class TestIsDir(unittest.TestCase):
         with NamedTemporaryFile(dir=self.test_dir) as temp_file:
             file_path = os.path.join(self.test_dir, temp_file.name)
             self.assertFalse(is_dir(file_path))
+
+
+class TestConfigureLogger(unittest.TestCase):
+    """ Tests for the configure_logger utility function """
+
+    def test_configure_logger(self):
+        """ Test that configure_logger configured the logger as expected """
+        mock_logger = MagicMock()
+
+        configure_logger(mock_logger, logging.INFO)
+
+        mock_logger.setLevel.assert_called_once_with(logging.INFO)
+        mock_logger.addHandler.assert_called_once()
diff --git a/tests/test_main.py b/tests/test_main.py
index f078fbb..223b7d7 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -20,6 +20,26 @@ class TestMainCli(unittest.TestCase):
         result = self.runner.invoke(cli, ["foo"])
         self.assertEqual(result.exit_code, -1)
 
+    def test_cli_verbose(self):
+        """ Test that cli sets the log level to info when given --verbose/-v """
+        for verbose_flag in ["--verbose", "-v"]:
+            with self.runner.isolated_filesystem() as fs:
+                source_file = NamedTemporaryFile(dir=fs, suffix=".txt", delete=False)
+                target_name = os.path.join(fs, "foo.txt")
+                result = self.runner.invoke(cli, [verbose_flag, source_file.name, target_name])
+
+                self.assertEqual(result.exit_code, 0)
+
+    def test_cli_debug(self):
+        """ Test that cli sets the log level to debug when given --debug/-d """
+        with self.runner.isolated_filesystem() as fs:
+            source_file = NamedTemporaryFile(dir=fs, suffix=".txt", delete=False)
+            target_name = os.path.join(fs, "foo.txt")
+            result = self.runner.invoke(cli, ["--debug", source_file.name, target_name])
+
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("DEBUG", result.output)
+
     def test_cli_rename_file(self):
         """ Test that cli renames a file when given 1 source and 1 target (not dir) """
         with self.runner.isolated_filesystem() as fs: