From 05eef0dff4a814c30f3208f8c42b1744eca51414 Mon Sep 17 00:00:00 2001 From: John Craig Date: Thu, 3 Oct 2024 10:21:37 -0400 Subject: [PATCH 1/2] Add support for user-defined macros Signed-off-by: John Craig --- docs/macros.md | 18 +++++++++++++++ examples/macros/logon.py | 31 ++++++++++++++++++++++++++ tnz/zti.py | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 docs/macros.md create mode 100644 examples/macros/logon.py diff --git a/docs/macros.md b/docs/macros.md new file mode 100644 index 0000000..7f72149 --- /dev/null +++ b/docs/macros.md @@ -0,0 +1,18 @@ +--- +title: User Macros +--- + +Users may define macros for `zti`. When defined, user macros appear in the list of available commands for `zti`. User macros are created by placing Python files in the ZTI macros directory, which defaults to the path `~/.zti-macros.d`. This path may be overridden with the `ZTI_MACROS_DIR` environment variables. + +User macro file names must take the form `my_macro.py`, and must contain a function with the signature, + +```python + def my_macro(zti, arg): + ... +``` + +This function is invoked when the macro command is run from `zti`. It is passed the current `zti` instance as the first argument and any arguments to the command as the second argument. + +See `examples/macros/logon.py` for an example of a user macro. + +A macro file name may not contain more than one period character. User macros which conflict with existing ZTI commands are ignored. \ No newline at end of file diff --git a/examples/macros/logon.py b/examples/macros/logon.py new file mode 100644 index 0000000..6d6208d --- /dev/null +++ b/examples/macros/logon.py @@ -0,0 +1,31 @@ +from tnz.ati import ati + +def logon(zti, arg): + logon_setup() + + ati.wait(lambda: ati.scrhas("Enter: ")) + ati.send("app1 userid[enter]") + ati.wait(lambda: ati.scrhas("Password ===> ")) + ati.send("password[enter]") + + # maybe look for a status message + ati.wait(lambda: ati.scrhas("ICH70001I")) + + # do something useful + ati.wait(lambda: ati.scrhas("***")) + ati.send("[enter]") + ati.wait(lambda: ati.scrhas("READY")) + ati.send("logon[enter]") + ati.wait(lambda: ati.scrhas("LOGGED OFF")) + + # disconnect from host + ati.drop("SESSION") + +def logon_helper(): + ati.set("TRACE", "ALL") + ati.set("LOGDEST", "example.log") + + ati.set("ONERROR", "1") + ati.set("DISPLAY", "HOST") + ati.set("SESSION_HOST", "mvs1") + ati.set("SESSION", "A") \ No newline at end of file diff --git a/tnz/zti.py b/tnz/zti.py index fda73cc..8776641 100644 --- a/tnz/zti.py +++ b/tnz/zti.py @@ -29,6 +29,7 @@ ZTI_AUTOSIZE ZTI_SECLEVEL (see tnz.py) ZTI_TITLE + ZTI_MACROS_DIR (~/.zti-macros.d is default) _BPX_TERMPATH (see _termlib.py) Copyright 2021, 2024 IBM Inc. All Rights Reserved. @@ -115,6 +116,11 @@ def __init__(self, stdin=None, stdout=None): if os.getenv("ZTI_AUTOSIZE"): self.autosize = True + self.macros_dir = os.getenv("ZTI_MACROS_DIR") + if self.macros_dir is None or not os.path.isdir( + self.macros_dir): + self.macros_dir = os.path.expanduser("~/.zti-macros.d") + self.pend_intro = None self.__dirty_ranges = [] self.single_session = False @@ -169,6 +175,8 @@ def __init__(self, stdin=None, stdout=None): self.__install_plugins() + self.__register_macros() + # Methods def atexit(self): @@ -1838,6 +1846,45 @@ def help_plugin(entry=entry): self.plugins = " ".join(plugins) + def __register_macros(self): + import importlib.util + import sys + import types + + if not os.path.isdir(self.macros_dir): + return + + for macro_file in os.listdir(self.macros_dir): + if not macro_file.endswith(".py"): + continue + + if len(macro_file.split('.')) > 1: + continue + + macro_name = macro_file.split('.')[0] + + # Ignore macros which already exist + if f"do_{macro_name}" in self.get_names(): + continue + + macro_path = os.path.join(self.macros_dir, macro_file) + + # Import the user macro as a module + macro_spec = importlib.util.spec_from_file_location( + f"module.{macro_name}", macro_path) + macro_module = importlib.util.module_from_spec(macro_spec) + sys.modules[f"module.{macro_name}"] = macro_module + macro_spec.loader.exec_module(macro_module) + + # Find the function + if hasattr(macro_module, macro_name): + # Create a new bound method for the `Zti` class for this + # function + setattr(Zti, f"do_{macro_name}", + types.MethodType( + getattr(macro_module, macro_name), + self)) + def __key_data(self, tns, data): try: tns.key_data(data, zti=self) From e9775fe372f0c2e744e5282e3dc820601cc140a0 Mon Sep 17 00:00:00 2001 From: John Craig Date: Mon, 7 Oct 2024 11:29:15 -0400 Subject: [PATCH 2/2] Code review changes Signed-off-by: John Craig --- docs/macros.md | 13 +++++-- tnz/zti.py | 92 ++++++++++++++++++++++++++++---------------------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/docs/macros.md b/docs/macros.md index 7f72149..85ee04b 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -2,17 +2,26 @@ title: User Macros --- -Users may define macros for `zti`. When defined, user macros appear in the list of available commands for `zti`. User macros are created by placing Python files in the ZTI macros directory, which defaults to the path `~/.zti-macros.d`. This path may be overridden with the `ZTI_MACROS_DIR` environment variables. +Users may define macros for `zti`. When defined, user macros appear in the list of available commands for `zti`. User macros are created by placing Python files in the ZTI macros directory, which defaults to the path `~/.zti-mac`. This path may be overridden with the `ZTI_MACROS_DIR` environment variables. User macro file names must take the form `my_macro.py`, and must contain a function with the signature, ```python - def my_macro(zti, arg): + def do_my_macro(zti, arg): ... ``` This function is invoked when the macro command is run from `zti`. It is passed the current `zti` instance as the first argument and any arguments to the command as the second argument. +The macro file may optionally also contain a function with the signature, + +```python + def help_my_macro(zti): + ... +``` + +This function will be registered as the help command for the main macro command. + See `examples/macros/logon.py` for an example of a user macro. A macro file name may not contain more than one period character. User macros which conflict with existing ZTI commands are ignored. \ No newline at end of file diff --git a/tnz/zti.py b/tnz/zti.py index 8776641..a9c4c91 100644 --- a/tnz/zti.py +++ b/tnz/zti.py @@ -29,7 +29,7 @@ ZTI_AUTOSIZE ZTI_SECLEVEL (see tnz.py) ZTI_TITLE - ZTI_MACROS_DIR (~/.zti-macros.d is default) + ZTI_MACROS_DIR (~/.zti-mac is default) _BPX_TERMPATH (see _termlib.py) Copyright 2021, 2024 IBM Inc. All Rights Reserved. @@ -119,7 +119,7 @@ def __init__(self, stdin=None, stdout=None): self.macros_dir = os.getenv("ZTI_MACROS_DIR") if self.macros_dir is None or not os.path.isdir( self.macros_dir): - self.macros_dir = os.path.expanduser("~/.zti-macros.d") + self.macros_dir = os.path.expanduser("~/.zti-mac") self.pend_intro = None self.__dirty_ranges = [] @@ -1846,45 +1846,6 @@ def help_plugin(entry=entry): self.plugins = " ".join(plugins) - def __register_macros(self): - import importlib.util - import sys - import types - - if not os.path.isdir(self.macros_dir): - return - - for macro_file in os.listdir(self.macros_dir): - if not macro_file.endswith(".py"): - continue - - if len(macro_file.split('.')) > 1: - continue - - macro_name = macro_file.split('.')[0] - - # Ignore macros which already exist - if f"do_{macro_name}" in self.get_names(): - continue - - macro_path = os.path.join(self.macros_dir, macro_file) - - # Import the user macro as a module - macro_spec = importlib.util.spec_from_file_location( - f"module.{macro_name}", macro_path) - macro_module = importlib.util.module_from_spec(macro_spec) - sys.modules[f"module.{macro_name}"] = macro_module - macro_spec.loader.exec_module(macro_module) - - # Find the function - if hasattr(macro_module, macro_name): - # Create a new bound method for the `Zti` class for this - # function - setattr(Zti, f"do_{macro_name}", - types.MethodType( - getattr(macro_module, macro_name), - self)) - def __key_data(self, tns, data): try: tns.key_data(data, zti=self) @@ -2741,6 +2702,55 @@ def __refresh(self): self.stdscr.refresh(_win_callback=self.__set_event_fn()) + def __register_macros(self): + import importlib.util + import sys + import types + + if not os.path.isdir(self.macros_dir): + _logger.exception(f"{self.macros_dir} is not a directory") + return + + for macro_file in os.listdir(self.macros_dir): + if not macro_file.endswith(".py"): + continue + + if len(macro_file.split('.')) > 1: + continue + + macro_name = macro_file.split('.')[0] + + # Ignore macros which already exist + if f"do_{macro_name}" in self.get_names(): + continue + + macro_path = os.path.join(self.macros_dir, macro_file) + + # Import the user macro as a module + macro_spec = importlib.util.spec_from_file_location( + f"module.{macro_name}", macro_path) + macro_module = importlib.util.module_from_spec(macro_spec) + sys.modules[f"module.{macro_name}"] = macro_module + macro_spec.loader.exec_module(macro_module) + + # Find the function + if hasattr(macro_module, f"do_{macro_name}"): + # Create a new bound method for the `Zti` class for this + # function + setattr(Zti, f"do_{macro_name}", + types.MethodType( + getattr(macro_module, f"do_{macro_name}"), + self)) + + # Check if a corresponding help function exists + if hasattr(macro_module, f"help_{macro_name}"): + # Create a new bound method for the `Zti` class for this + # function + setattr(Zti, f"help_{macro_name}", + types.MethodType( + getattr(macro_module, f"help_{macro_name}"), + self)) + def __scale_size(self, maxrow, maxcol): arows, acols = self.autosize aspect1 = arows / acols