Repository: pddg/uroboros

Issue:
Implement hook function to `uroboros.Option`
I forgot to implement it...

Discussion:

Patch:
diff --git a/uroboros/command.py b/uroboros/command.py
index 963cba2..1f15f8b 100644
--- a/uroboros/command.py
+++ b/uroboros/command.py
@@ -221,6 +221,13 @@ class Command(metaclass=abc.ABCMeta):
             self: commands_dict,
         }
 
+    def get_options(self) -> 'List[Option]':
+        """
+        Get all `Option` instance of this `Command`.
+        :return: List of Option instance
+        """
+        return self.options
+
     def print_help(self):
         """
         Helper method for print the help message of this command.
@@ -233,10 +240,25 @@ class Command(metaclass=abc.ABCMeta):
                   sub_commands: 'List[Command]') -> 'argparse.Namespace':
         return utils.call_one_by_one(
             [self] + sub_commands,
-            "before_validate",
-            args
+            "_hook",
+            args,
+            hook_name="before_validate"
         )
 
+    def _hook(self,
+              args: 'argparse.Namespace',
+              hook_name: str) -> 'argparse.Namespace':
+        for opt in self.get_options():
+            assert hasattr(opt, hook_name), \
+                "{} does not have '{}' method".format(
+                    opt.__class__.__name__, hook_name)
+            args = getattr(opt, hook_name)(args)
+        assert hasattr(self, hook_name), \
+            "{} does not have '{}' method".format(
+                self.__class__.__name__, hook_name)
+        args = getattr(self, hook_name)(args)
+        return args
+
     def before_validate(self,
                         unsafe_args: 'argparse.Namespace'
                         ) -> 'argparse.Namespace':
@@ -276,8 +298,9 @@ class Command(metaclass=abc.ABCMeta):
                             ) -> 'argparse.Namespace':
         return utils.call_one_by_one(
             [self] + sub_commands,
-            "after_validate",
-            args
+            "_hook",
+            args,
+            hook_name='after_validate'
         )
 
     def after_validate(self,
diff --git a/uroboros/option.py b/uroboros/option.py
index 1707b2a..bd30d45 100644
--- a/uroboros/option.py
+++ b/uroboros/option.py
@@ -19,6 +19,15 @@ class Option(metaclass=abc.ABCMeta):
             -> 'argparse.ArgumentParser':
         raise NotImplementedError
 
-    @abc.abstractmethod
+    def before_validate(self,
+                        unsafe_args: 'argparse.Namespace'
+                        ) -> 'argparse.Namespace':
+        return unsafe_args
+
     def validate(self, args: 'argparse.Namespace') -> 'List[Exception]':
-        raise NotImplementedError
+        raise []
+
+    def after_validate(self,
+                       safe_args: 'argparse.Namespace'
+                       ) -> 'argparse.Namespace':
+        return safe_args
diff --git a/uroboros/utils.py b/uroboros/utils.py
index 04b4c6e..6a90229 100644
--- a/uroboros/utils.py
+++ b/uroboros/utils.py
@@ -10,12 +10,12 @@ def get_args_section_name(layer: int):
     return "__layer{layer}_parser".format(layer=layer)
 
 
-def call_one_by_one(objs, method_name: str, args):
+def call_one_by_one(objs, method_name: str, args, **kwargs):
     for obj in objs:
         assert hasattr(obj, method_name), \
             "'{cmd}' has no method '{method}".format(
                 cmd=obj.__name__,
                 method=method_name
             )
-        args = getattr(obj, method_name)(args)
+        args = getattr(obj, method_name)(args, **kwargs)
     return args


Test patch:
diff --git a/tests/test_option.py b/tests/test_option.py
new file mode 100644
index 0000000..8292b58
--- /dev/null
+++ b/tests/test_option.py
@@ -0,0 +1,90 @@
+import argparse
+
+import pytest
+
+import uroboros
+
+
+class SampleOption(uroboros.Option):
+
+    name = 'option'
+    value = 'option'
+
+    def build_option(self,
+                     parser: 'argparse.ArgumentParser'
+                     ) -> 'argparse.ArgumentParser':
+        parser.add_argument('--{}'.format(self.name),
+                            default=self.value,
+                            type=str)
+        return parser
+
+    def validate(self,
+                 args: 'argparse.Namespace'
+                 ) -> 'List[Exception]':
+        if getattr(args, self.name) != self.value:
+            return [Exception("{} is expected".format(self.value))]
+        return []
+
+    def before_validate(self,
+                        unsafe_args: 'argparse.Namespace'
+                        ) -> 'argparse.Namespace':
+        setattr(
+            unsafe_args,
+            'before_validate_{}'.format(self.name),
+            self.value
+        )
+        return unsafe_args
+
+    def after_validate(self, safe_args):
+        setattr(
+            safe_args,
+            'after_validate_{}'.format(self.name),
+            self.value
+        )
+        return safe_args
+
+
+class NoHookOption(uroboros.Option):
+    name = 'nohook'
+    value = 'nohook'
+
+    def build_option(self,
+                     parser: 'argparse.ArgumentParser'
+                     ) -> 'argparse.ArgumentParser':
+        parser.add_argument("--{}".format(self.name), default=self.value)
+        return parser
+
+
+class TestOption(object):
+
+    def test_no_before_validate(self):
+        args = argparse.Namespace()
+        nohook = NoHookOption()
+        assert nohook.before_validate(args) == args
+
+    def test_before_hook(self):
+        args = argparse.Namespace()
+        opt = SampleOption()
+        hooked_args = opt.after_validate(args)
+        actual = getattr(
+            hooked_args, "after_validate_{}".format(opt.name))
+        assert actual == opt.value
+
+    def test_no_after_validate(self):
+        args = argparse.Namespace()
+        nohook = NoHookOption()
+        assert nohook.before_validate(args) == args
+
+    def test_after_hook(self):
+        args = argparse.Namespace()
+        opt = SampleOption()
+        hooked_args = opt.after_validate(args)
+        actual = getattr(
+            hooked_args, "after_validate_{}".format(opt.name))
+        assert actual == opt.value
+
+    def test_cannot_instantiate(self):
+        class Opt(uroboros.Option):
+            pass
+        with pytest.raises(TypeError):
+            Opt()