From df6903212072646421e69694e08f373130ad29e9 Mon Sep 17 00:00:00 2001 From: Benjamin Fagin Date: Mon, 12 Aug 2019 16:11:51 -0700 Subject: [PATCH] initial support for inheritence (#11) --- .gitignore | 2 ++ README.md | 11 +++++---- class_test.py | 11 +++++++++ class_test_books.py | 18 +++++++++++++++ runbook.py | 54 ++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 class_test.py create mode 100644 class_test_books.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8c80bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/__pycache__/* +*.log diff --git a/README.md b/README.md index f6d003e..be03d44 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,22 @@ if __name__ == '__main__': CustomRunbook.main() ``` -The run-book object can also be instantiated and run directly, like so: +The run-book object can also be instantiated and run directly. ```python book = CustomRunbook(file_path="path/to/file") book.run() ``` -You should avoid using the step names `run` and `main`, which are already defined. +**You should avoid using the step names `run` and `main`**, which are already defined. If you need to override these +methods to define custom behavior that is fine. -As steps are completed, the results are written out to a log file. You can set a custom log file path by passing an argument to main, as in: +As steps are completed, the results are written out to a log file. You can set a custom log file path by passing +an argument to main, as in: ``` python3 my_runbook.py output.log ``` -When using the same log file, already completed steps will be skipped. Any new steps found in the `Runbook` class and not in the log will be processed. \ No newline at end of file +When reusing the same log file, already completed steps will be skipped. Any new steps found in the `Runbook` +and not already in the log will be processed as normal, with results appended to the end of the log file. \ No newline at end of file diff --git a/class_test.py b/class_test.py new file mode 100644 index 0000000..2b61345 --- /dev/null +++ b/class_test.py @@ -0,0 +1,11 @@ +from class_test_books import BookA, BookB + + +class BookC(BookB, BookA): + + def step_c4(): + pass + + +if __name__ == '__main__': + BookC.main() \ No newline at end of file diff --git a/class_test_books.py b/class_test_books.py new file mode 100644 index 0000000..6b9d294 --- /dev/null +++ b/class_test_books.py @@ -0,0 +1,18 @@ +from runbook import Runbook + + + +class BookA(Runbook): + + def step_b1(): + pass + + +class BookB(Runbook): + + def step_a2(self): + pass + + @staticmethod + def step_a3(): + pass \ No newline at end of file diff --git a/runbook.py b/runbook.py index 3efcf70..59b53f6 100644 --- a/runbook.py +++ b/runbook.py @@ -133,13 +133,50 @@ def _wait_for_response(self): print("\n\tinvalid response\n") + def _get_steps(self) -> List[Step]: + + # list class hierarchy, in order + classes = list(inspect.getmro(type(self))) + classes.reverse() + + # list methods by class, in order + methods_by_class = {} + all_methods_by_class = {} - # get all methods in the class - methods = inspect.getmembers(self, predicate=inspect.ismethod) + for c in classes: + methods_by_class[c] = inspect.getmembers(c, lambda _:inspect.ismethod(_) or inspect.isfunction(_)) + all_methods_by_class[c] = [] # sort methods by declaration order - methods = sorted(methods, key=lambda _: _[1].__func__.__code__.co_firstlineno) + def key_filter(value): + value = value[1] + + if hasattr(value, '__func__'): + return value.__func__.__code__.co_firstlineno + else: + return value.__code__.co_firstlineno + + all_methods = inspect.getmembers(self, lambda _:inspect.ismethod(_) or inspect.isfunction(_)) + all_methods = sorted(all_methods, key=key_filter) + + for n1, m1 in all_methods: + for clazz, class_methods in methods_by_class.items(): + should_continue = True + + for n2, m2 in class_methods: + if n1 == n2: + all_methods_by_class[clazz].append((n1,m1)) + should_continue = False + break + + if should_continue is False: + break + + methods = [] + + for a, b in all_methods_by_class.items(): + methods.extend(b) # build up a list of steps steps:List[Step] = [] @@ -157,7 +194,8 @@ def _get_steps(self) -> List[Step]: # if method is zero arg, call the unbound class method # (as a convenience for @staticmethod) - function_signature = inspect.signature(method.__func__) + function = method.__func__ if hasattr(method, '__func__') else method + function_signature = inspect.signature(function) if len(function_signature.parameters) == 0: method = getattr(type(self), method_name) @@ -210,6 +248,8 @@ def _read_file(file_path): def _write_result(self, step:Step, result, negative=False, reason=None): + + # open a file in unicode append mode with open(self.file_path, "a+") as file: # write name header @@ -229,7 +269,11 @@ def _write_result(self, step:Step, result, negative=False, reason=None): file.write("\n") # write generic response line - file.write(f"responded `{result}` at {datetime.now().strftime('%H:%M:%S')} on {datetime.now().strftime('%d/%m/%Y')}\n") + file.write( + f"responded `{result}` " + f"at {datetime.now().strftime('%H:%M:%S')} " + f"on {datetime.now().strftime('%d/%m/%Y')}\n" + ) # write negative response line if negative is True and reason: