From 81f96873c8c249bbc9ae7f97bb41e0b295e7c210 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 18 Nov 2020 21:52:17 +0800 Subject: [PATCH 1/4] cosmetic, improve loading message --- allzpark/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allzpark/cli.py b/allzpark/cli.py index 058d84f..ac1422b 100644 --- a/allzpark/cli.py +++ b/allzpark/cli.py @@ -243,10 +243,10 @@ def initialize(config_file=None, "start anew") if clean: - tell("(clean) ") + tell("(clean) ", newlines=0) storage.clear() else: - tell("(%s)" % storage.fileName()) + tell("(%s) " % storage.fileName(), newlines=0) defaults = { "memcachedURI": os.getenv("REZ_MEMCACHED_URI", "None"), From 145de88703e39fd2b77038ccddb5737985ea0476 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 18 Nov 2020 21:58:34 +0800 Subject: [PATCH 2/4] add environment plugin base class --- allzpark/plugin.py | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 allzpark/plugin.py diff --git a/allzpark/plugin.py b/allzpark/plugin.py new file mode 100644 index 0000000..d1e1fa0 --- /dev/null +++ b/allzpark/plugin.py @@ -0,0 +1,129 @@ + +import logging +from .vendor.Qt import QtCore, QtWidgets + + +class EnvPluginBase(QtWidgets.QWidget): + """Base class of environment plugin""" + + name = "Environment Plugin" + + envChanged = QtCore.Signal(dict) + envReset = QtCore.Signal() + revealed = QtCore.Signal() + consoleShown = QtCore.Signal() + logged = QtCore.Signal(str, int) # message, level + + def on_profile_changed(self, package): + """Runs on profile changed + + This will be called by Allzpark when profile has changed. + Reimplement this function if you need plugin to take actions on + profile change. + + Args: + package: profile package + + """ + pass + + def on_application_changed(self, package): + """Runs on application selection changed + + This will be called by Allzpark when selected application changed. + Reimplement this function if you need plugin to take actions on + application change. + + Args: + package: application package + + """ + pass + + def validate(self, environ, package): + """Validate environment before launching application + + This will be called by Allzpark when application is about to launch, + and abort launching if validation failed with message returned. + + Return None if validation passed, or string message as reason why it + fails. Returning any value that is not equivalent to False (e.g. None, + "", 0) will be considered as fail and the value will be used to format + string error message. + + Args: + environ (dict): env vars that merged from parent, plugin and user + package: application package that being launched + + Returns: + None or str + + """ + pass + + def set_env(self, env): + """Inject additional environment variables + + The `env` will be applied on top of parent environment, and user + environment variables on top of it. + + Multi-path variable will simply be overwritten, no appending nor + prepending. + + Args: + env (dict): key-value paired environment variables + + Returns: + None + + """ + self.envChanged.emit(env) + + def clear_env(self): + """Reset additional environment variables + + Returns: + None + + """ + self.envReset.emit() + + def reveal(self): + """Activate plugin dock widget + + Show plugin dock widget. Could be used when input is required and + need to have focus on the plugin page. + + Returns: + None + + """ + self.revealed.emit() + + def to_console(self): + """Activate Allzpark console dock widget + + Show Allzpark console dock widget. Could be used when there are log + messages that need user to know. + + Returns: + None + + """ + self.consoleShown.emit() + + def debug(self, message): + """Send debug message to Allzpark console""" + self.logged.emit(message, logging.DEBUG) + + def info(self, message): + """Send regular message to Allzpark console""" + self.logged.emit(message, logging.INFO) + + def warning(self, message): + """Send warning message to Allzpark console""" + self.logged.emit(message, logging.WARNING) + + def error(self, message): + """Send error message to Allzpark console""" + self.logged.emit(str(message), logging.ERROR) From fb55751c477244bed15b77ffbce426c61ee1a81d Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 18 Nov 2020 22:02:30 +0800 Subject: [PATCH 3/4] implement env plugin dock widget --- allzpark/allzparkconfig.py | 21 ++++++++++ allzpark/cli.py | 13 +++++- allzpark/control.py | 16 ++++++- allzpark/dock.py | 86 +++++++++++++++++++++++++++++++++++++- allzpark/view.py | 57 ++++++++++++++++++------- 5 files changed, 172 insertions(+), 21 deletions(-) diff --git a/allzpark/allzparkconfig.py b/allzpark/allzparkconfig.py index 0c459f0..925ee65 100644 --- a/allzpark/allzparkconfig.py +++ b/allzpark/allzparkconfig.py @@ -126,6 +126,27 @@ def protected_preferences(): return dict() +def environment_plugin(): + """Custom widget for adding extra environment variables + + In some scenario, e.g. production pipeline involved workflow, may + require passing additional arguments as environment variables to + application that is being launched. And those environment variables, + like shot numbers, may change regularly so can't be fixed in Rez + package. + + To adopt that, one could subclass `allzpark.plugin.EnvPluginBase` + to implement a custom widget that can be hooked with Allzpark, as + an additional interface for user to make production decisions, and + parse those inputs into application's launching environment. + + Returns: + None or subclass of `allzpark.plugin.EnvPluginBase` + + """ + return None + + def themes(): """Allzpark GUI theme list provider diff --git a/allzpark/cli.py b/allzpark/cli.py index ac1422b..caed8d9 100644 --- a/allzpark/cli.py +++ b/allzpark/cli.py @@ -293,7 +293,7 @@ def initialize(config_file=None, def launch(ctrl): - from . import view, resources, util + from . import view, dock, resources, util # Handle stdio from within the application if necessary if hasattr(allzparkconfig, "__noconsole__"): @@ -318,7 +318,16 @@ def excepthook(type, value, traceback): with timings("- Loading themes.. "): resources.load_themes() - window = view.Window(ctrl) + with timings("- Loading environment plugin.. ") as msg: + plugin_cls = allzparkconfig.environment_plugin() + if plugin_cls is None: + plugin = None + msg["success"] = "no plugin - ok\n" + else: + plugin = dock.EnvironmentPlugin(ctrl, plugin_cls) + msg["success"] = "%s loaded - ok {:.2f}\n" % plugin.name + + window = view.Window(ctrl, plugin) user_css = ctrl.state.retrieve("userCss", "") originalcss = resources.load_theme(ctrl.state.retrieve("theme")) # Store for CSS Editor diff --git a/allzpark/control.py b/allzpark/control.py index 7274cb1..ebb70f2 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -74,6 +74,8 @@ def __init__(self, ctrl, storage, parent_environ=None): # Cache environment testing result "testedEnvirons": {}, + "pluginEnvironValidator": lambda *args: None, + "rezApps": odict(), "fullCommand": "rez env", "serialisationMode": ( @@ -237,6 +239,7 @@ def __init__(self, "context": model.ContextModel(), "environment": model.EnvironmentModel(), "parentenv": model.EnvironmentModel(), + "plugin": model.EnvironmentModel(), "diagnose": model.EnvironmentModel(), "commands": model.CommandsModel(), } @@ -309,6 +312,8 @@ def context(self, app_request): def parent_environ(self): environ = self._state["parentEnviron"].copy() + # Inject plugin environment + environ = dict(environ, **(self._models["plugin"].json() or {})) # Inject user environment # # NOTE: Rez takes precendence on environment, so a user @@ -511,6 +516,9 @@ def on_unhandled_exception(self, type, value, tb): # Methods # ---------------- + def register_environment_validator(self, validator): + self._state["pluginEnvironValidator"] = validator + def stdio(self, stream, level=logging.INFO): return _Stream(self, stream, level) @@ -772,6 +780,13 @@ def do(): disabled = self._models["packages"]._disabled environ = self.parent_environ() + validator = self._state["pluginEnvironValidator"] + invalid = validator(environ, rez_app) + if invalid: + self.error("Plugin environment validation failed:\n" + "%s" % invalid) + return + self.debug( "Launching %s%s.." % ( tool_name, " (detached)" if is_detached else "") @@ -1269,7 +1284,6 @@ def graph(self): context = self._state["rezContexts"][self._state["appRequest"]] if isinstance(context, model.BrokenContext): self._state.to_console() - self._state.to_ready() self.error("Can not graph a broken context.") return diff --git a/allzpark/dock.py b/allzpark/dock.py index 72c8a15..ad6fb97 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -702,6 +702,7 @@ def __init__(self, ctrl, parent=None): "environment": QtWidgets.QWidget(), "editor": EnvironmentEditor(), "penv": QtWidgets.QWidget(), + "plugin": QtWidgets.QWidget(), "diagnose": QtWidgets.QWidget(), } @@ -709,6 +710,7 @@ def __init__(self, ctrl, parent=None): "view": JsonView(), "penv": JsonView(), "test": JsonView(), + "plugin": JsonView(), "compute": QtWidgets.QPushButton("Compute Environment"), } @@ -720,12 +722,16 @@ def __init__(self, ctrl, parent=None): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(widgets["penv"]) + layout = QtWidgets.QVBoxLayout(pages["plugin"]) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(widgets["plugin"]) + layout = QtWidgets.QVBoxLayout(pages["diagnose"]) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(widgets["test"]) layout.addWidget(widgets["compute"]) - for view in ["view", "penv", "test"]: + for view in ["view", "penv", "plugin", "test"]: widgets[view].setSortingEnabled(True) widgets[view].sortByColumn(0, QtCore.Qt.AscendingOrder) @@ -733,9 +739,12 @@ def __init__(self, ctrl, parent=None): panels["central"].addTab(pages["environment"], "Context") panels["central"].addTab(pages["penv"], "Parent") + panels["central"].addTab(pages["plugin"], "Plugin") panels["central"].addTab(pages["editor"], "User") panels["central"].addTab(pages["diagnose"], "Diagnose") + panels["central"].setTabVisible(2, False) + user_env = ctrl.state.retrieve("userEnv", {}) pages["editor"].from_environment(user_env) pages["editor"].warning.connect(self.on_env_warning) @@ -750,10 +759,14 @@ def __init__(self, ctrl, parent=None): self._models = { "environ": None, "parent": None, + "plugin": None, "diagnose": None, } - def set_model(self, environ, parent, diagnose): + def enable_plugin(self): + self._panels["central"].setTabVisible(2, True) + + def set_model(self, environ, parent, plugin, diagnose): proxy_model = QtCore.QSortFilterProxyModel() proxy_model.setSourceModel(environ) self._widgets["view"].setModel(proxy_model) @@ -764,6 +777,11 @@ def set_model(self, environ, parent, diagnose): self._widgets["penv"].setModel(proxy_model) self._models["parent"] = parent + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(plugin) + self._widgets["plugin"].setModel(proxy_model) + self._models["plugin"] = plugin + proxy_model = QtCore.QSortFilterProxyModel() proxy_model.setSourceModel(diagnose) self._widgets["test"].setModel(proxy_model) @@ -1768,3 +1786,67 @@ def update_current(self, profile, version, icon): icon = res.icon(icon) icon = icon.pixmap(QtCore.QSize(px(32), px(32))) self._widgets["icon"].setPixmap(icon) + + +class EnvironmentPlugin(AbstractDockWidget): + """Interface for adding extra environment variables""" + name = "Environment plugin" + icon = "File_Query_32" + + revealed = QtCore.Signal() + + def __init__(self, ctrl, plugin_cls, parent=None): + # update dock name and docstring from plugin class + self.name = getattr(plugin_cls, "name", self.name) + self.__doc__ = plugin_cls.__doc__ or self.__doc__ + super(EnvironmentPlugin, self).__init__(self.name, parent) + self.setAttribute(QtCore.Qt.WA_StyledBackground) + self.setObjectName(self.name) + + panels = { + "central": QtWidgets.QWidget(), + } + + widgets = { + "plugin": plugin_cls(), + } + + layout = QtWidgets.QVBoxLayout(panels["central"]) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(widgets["plugin"]) + + # update icon for plugin instance that ships it's own QIcon + self.icon = getattr(widgets["plugin"], "icon", self.icon) + self._ctrl = ctrl + self._widgets = widgets + + self.setWidget(panels["central"]) + + def connect_plugin(self): + ctrl = self._ctrl + plugin_ = self._widgets["plugin"] + + ctrl.register_environment_validator(plugin_.validate) + + ctrl.profile_changed.connect(self.on_profile_changed) + ctrl.application_changed.connect(self.on_application_changed) + + plugin_.envChanged.connect(ctrl.models["plugin"].load) + plugin_.envReset.connect(ctrl.models["plugin"].reset) + plugin_.revealed.connect(self.revealed.emit) + plugin_.consoleShown.connect(ctrl.state.to_console) + plugin_.logged.connect(ctrl.logged.emit) + + def on_profile_changed(self, name, version, *args, **kwargs): + plugin_ = self._widgets["plugin"] + + profile_versions = self._ctrl.state["rezProfiles"][name] + profile_package = profile_versions[version] + plugin_.on_profile_changed(profile_package) + + def on_application_changed(self): + plugin_ = self._widgets["plugin"] + + app_request = self._ctrl.state["appRequest"] + app_package = self._ctrl.state["rezApps"][app_request] + plugin_.on_application_changed(app_package) diff --git a/allzpark/view.py b/allzpark/view.py index 2a7e7c3..20629e0 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -63,7 +63,7 @@ def is_selected_app_ok(self): class Window(QtWidgets.QMainWindow): title = "Allzpark %s" % version - def __init__(self, ctrl, parent=None): + def __init__(self, ctrl, plugin=None, parent=None): super(Window, self).__init__(parent) self.setWindowTitle(self.title) self.setAttribute(QtCore.Qt.WA_StyledBackground) @@ -129,6 +129,9 @@ def __init__(self, ctrl, parent=None): ("commands", dock.Commands()), ("preferences", dock.Preferences(self, ctrl)), )) + if plugin is not None: + docks["plugin"] = plugin + docks["environment"].enable_plugin() # Expose to CSS for name, widget in chain(panels.items(), @@ -226,8 +229,10 @@ def addColumn(widgets, *args, **kwargs): for name, widget in docks.items(): has_menu = hasattr(widget, "on_context_menu") - BtnCls = (dock.PushButtonWithMenu - if has_menu else QtWidgets.QPushButton) + BtnCls = (dock.PushButtonWithMenu if has_menu + else QtWidgets.QPushButton) + btn_icon = (widget.icon if isinstance(widget.icon, QtGui.QIcon) + else res.icon(widget.icon)) toggle = BtnCls() toggle.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) @@ -235,7 +240,7 @@ def addColumn(widgets, *args, **kwargs): toggle.setCheckable(True) toggle.setFlat(True) toggle.setProperty("type", "toggle") - toggle.setIcon(res.icon(widget.icon)) + toggle.setIcon(btn_icon) toggle.setIconSize(QtCore.QSize(px(32), px(32))) toggle.setToolTip("\n".join([ type(widget).__name__, widget.__doc__ or ""])) @@ -267,7 +272,7 @@ def on_visible(widget, toggle, state): # Forward any messages widget.message.connect(self.tell) - _section = "left" if name == "profiles" else "dock" + _section = "left" if name in ["profiles", "plugin"] else "dock" _layouts[_section].addWidget(toggle) layout = QtWidgets.QVBoxLayout(panels["body"]) @@ -296,6 +301,7 @@ def on_visible(widget, toggle, state): docks["context"].set_model(ctrl.models["context"]) docks["environment"].set_model(ctrl.models["environment"], ctrl.models["parentenv"], + ctrl.models["plugin"], ctrl.models["diagnose"]) docks["commands"].set_model(ctrl.models["commands"]) @@ -314,6 +320,13 @@ def on_visible(widget, toggle, state): self.on_profileversion_changed) docks["profiles"].reset.connect(self.reset) + if "plugin" in docks: + def on_reveal_plugin(): + docks["plugin"].show() + self.on_dock_toggled(docks["plugin"], visible=True) + docks["plugin"].connect_plugin() + docks["plugin"].revealed.connect(on_reveal_plugin) + widgets["reset"].clicked.connect(self.on_reset_clicked) widgets["continue"].clicked.connect(self.on_continue_clicked) widgets["apps"].activated.connect(self.on_app_clicked) @@ -404,14 +417,18 @@ def setup_docks(self): profile = docks[0] self.addDockWidget(area, profile) + if "plugin" in self._docks: + plugin = docks[-1] + self.addDockWidget(area, plugin) + right_docks = docks[1:-1] + else: + right_docks = docks[1:] + area = QtCore.Qt.RightDockWidgetArea - first = docks[1] + first = right_docks[0] self.addDockWidget(area, first) - for widget in docks[2:]: - if widget is first: - continue - + for widget in right_docks[1:]: self.addDockWidget(area, widget) self.tabifyDockWidget(first, widget) @@ -530,15 +547,20 @@ def on_dock_toggled(self, dock, visible): if ctrl_held or not allow_multiple: ignore_allow_multiple = [ # docks that are not restricted by this rule - "profiles", + self._docks["profiles"], + self._docks["plugin"], ] + all_docks = self._docks.values() - for name, d in self._docks.items(): - if name in ignore_allow_multiple: - continue - d.setVisible(d == dock) + if dock in ignore_allow_multiple: + dock.setVisible(True) + else: + for d in all_docks: + if d in ignore_allow_multiple: + continue + d.setVisible(d == dock) - if len([d for d in self._docks.values() if d.isVisible()]) <= 1: + if len([d for d in all_docks if d.isVisible()]) <= 1: # Only one or no visible dock return @@ -705,6 +727,9 @@ def on_state_changed(self, state): self._widgets["apps"].setEnabled(False) self._docks["app"].on_state_appfailed() + if state == "console": + self._widgets["apps"].setEnabled(True) + if state == "notresolved": pass From 719aeb83a13282606a2c42740d344994b180586d Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 18 Nov 2020 22:14:59 +0800 Subject: [PATCH 4/4] fix KeyError when no plugin provided --- allzpark/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allzpark/view.py b/allzpark/view.py index 20629e0..2cb8257 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -547,8 +547,8 @@ def on_dock_toggled(self, dock, visible): if ctrl_held or not allow_multiple: ignore_allow_multiple = [ # docks that are not restricted by this rule - self._docks["profiles"], - self._docks["plugin"], + self._docks[name] for name in ["profiles", "plugin"] + if name in self._docks ] all_docks = self._docks.values()