From 4804851d2638247a33f0a140a6a21ee4330435cd Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 11 Nov 2020 23:55:02 +0800 Subject: [PATCH 01/28] model refactored and imp #127, #96 PackagesModel and ApplicationModel are merged into one ResolvedPackagesModel with ApplicationProxyModel and PackagesProxyModel added. Also, model doesn't need controller now. --- allzpark/control.py | 76 +++++--- allzpark/delegates.py | 2 +- allzpark/dock.py | 21 ++- allzpark/model.py | 393 ++++++++++++++++++++---------------------- allzpark/view.py | 33 +++- 5 files changed, 278 insertions(+), 247 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 56c8f58..4fd0344 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -191,7 +191,7 @@ class Controller(QtCore.QObject): profile_changed = QtCore.Signal( str, object, bool) # profile, version, refreshed - application_changed = QtCore.Signal() + application_changed = QtCore.Signal(str) # The current command to launch an application has changed command_changed = QtCore.Signal(str) # command @@ -227,12 +227,11 @@ def __init__(self, state = State(self, storage, parent_environ) models = { - "apps": model.ApplicationModel(), + "resolved": model.ResolvedPackagesModel(), "profileVersions": QtCore.QStringListModel(), # Docks "profiles": model.ProfileModel(), - "packages": model.PackagesModel(self), "context": model.ContextModel(), "environment": model.EnvironmentModel(), "parentenv": model.EnvironmentModel(), @@ -354,9 +353,6 @@ def environ(self, app_request): env[app_request] = environ return environ - def resolved_packages(self, app_request): - return self._state["rezContexts"][app_request].resolved_packages - # ---------------- # Events # ---------------- @@ -719,7 +715,7 @@ def do(): rez_app.name, rez_app.version )) - app_model = self._models["apps"] + app_model = self._models["resolved"] app_index = app_model.findIndex(app_request) tool_name = kwargs.get( @@ -734,8 +730,8 @@ def do(): "This is a bug" ) - overrides = self._models["packages"]._overrides - disabled = self._models["packages"]._disabled + overrides = self._models["resolved"].overrides + disabled = self._models["resolved"].disabled environ = self.parent_environ() self.debug( @@ -810,7 +806,7 @@ def on_failure(error, trace): def delocalize(self, name): def do(): - item = self._models["packages"].find(name) + item = self._models["resolved"].find(name) package = item["package"] self.debug("Delocalizing %s" % package.root) localz.delocalize(package) @@ -872,11 +868,10 @@ def list_profiles(self, root=None): def select_profile(self, profile_name, version_name=Latest): # Wipe existing data - self._models["apps"].reset() + self._models["resolved"].reset() self._models["context"].reset() self._models["environment"].reset() self._models["diagnose"].reset() - self._models["packages"].reset() self._models["profileVersions"].setStringList([]) self._state["rezContexts"].clear() @@ -884,8 +879,8 @@ def select_profile(self, profile_name, version_name=Latest): self._state["testedEnvirons"].clear() self._state["rezApps"].clear() - def on_apps_found(apps): - if not apps: + def on_apps_found(packages): + if not packages: self._state["error"] = """

:(


@@ -905,7 +900,7 @@ def on_apps_found(apps): self._state.to_noapps() else: - self._models["apps"].reset(apps) + self._models["resolved"].reset(packages) self._state.to_ready() def on_apps_not_found(error, trace): @@ -968,28 +963,32 @@ def select_application(self, app_request): try: context = self.context(app_request) environ = self.environ(app_request) - packages = self.resolved_packages(app_request) diagnose = self._state["testedEnvirons"].get(app_request, {}) except Exception: - self._models["packages"].reset() self._models["context"].reset() self._models["environment"].reset() self._models["diagnose"].reset() raise - self._models["packages"].reset(packages) self._models["context"].load(context.to_dict()) self._models["environment"].load(environ) self._models["diagnose"].load(diagnose) - tools = self._models["apps"].find(app_request)["tools"] + tools = self._models["resolved"].find(app_request)["tools"] self._state["tool"] = tools[0] # Use this application on next launch or change of profile self.update_command() self._state.store("startupApplication", app_request) - self.application_changed.emit() + self.application_changed.emit(app_request) + + if context.success: + self._state.to_appok() + else: + self._state.to_appfailed() + + self._state.to_ready() if context.success: self._state.to_appok() @@ -1088,9 +1087,12 @@ def _list_apps(self, profile): patch_with_filter = self._state.retrieve("patchWithFilter", False) package_filter = self._package_filter() + app_ranges = dict() + def _try_finding_latest_app(req_str): req_str = req_str.strip("~") req = rez.PackageRequest(req_str) + app_ranges[req.name] = req.range try: return rez.find_latest(req.name, range_=req.range) except _missing as e_: @@ -1134,6 +1136,12 @@ def _try_resolve_context(req, pkg_name, mode): app_package.name, mode="Patch") + # update context key `app_request` if patched + for pkg in context.resolved_packages or []: + if pkg.name == app_package.name: + app_request = "%s==%s" % (pkg.name, pkg.version) + break + contexts[app_request] = context # Associate a Rez package with an app @@ -1181,10 +1189,16 @@ def _try_resolve_context(req, pkg_name, mode): self._state["rezApps"][app_request] = rez_pkg + self._state["rezContexts"] = contexts + self.debug("Resolved all contexts in %.2f seconds" % t.duration) - # Hide hidden - visible_apps = [] + visible_apps = dict() + + # * Opt-out hidden application + # * Find application versions + # * Context resolved packages (latest only) + paths = self._package_paths() show_hidden = self._state.retrieve("showHiddenApps") for request, package in self._state["rezApps"].items(): data = allzparkconfig.metadata_from_package(package) @@ -1193,9 +1207,23 @@ def _try_resolve_context(req, pkg_name, mode): if hidden and not show_hidden: continue - visible_apps += [package] + versions = rez.find(package.name, + range_=app_ranges[package.name], + paths=paths) + versions = sorted( + [str(v.version) for v in versions], + key=util.natural_keys + ) + resolved_packages = [ + pkg for pkg in contexts[request].resolved_packages + if pkg.name != package.name + ] + visible_apps[request] = { + "app": package, + "versions": versions, + "packages": resolved_packages, + } - self._state["rezContexts"] = contexts return visible_apps def graph(self): diff --git a/allzpark/delegates.py b/allzpark/delegates.py index c1c798c..e1d2202 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -24,7 +24,7 @@ def setEditorData(self, editor, index): def setModelData(self, editor, model, index): model = index.model() - package = model.data(index, "name") + package = model.data(index, "family") options = model.data(index, "versions") default = model.data(index, "default") version = options[editor.currentIndex()] diff --git a/allzpark/dock.py b/allzpark/dock.py index 3bea5da..c0c440d 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -174,7 +174,7 @@ def on_arg_changed(self, arg): return ctrl = self._ctrl - model = ctrl.models["apps"] + model = ctrl.models["resolved"] app_name = ctrl.state["appRequest"] app_index = model.findIndex(app_name) value = arg.read() @@ -361,19 +361,24 @@ def on_resetted(self): arg._previous = patch def set_model(self, model_): - proxy_model = model.ProxyModel(model_) + proxy_model = model.PackagesProxyModel(model_) + proxy_model.setup(include=[("request", None)]) self._widgets["view"].setModel(proxy_model) model_.modelReset.connect(self.on_model_changed) model_.dataChanged.connect(self.on_model_changed) + def on_app_changed(self, app_request): + model_ = self._widgets["view"].model() + model_.setup(include=[("request", app_request)]) + def on_model_changed(self): - model = self._widgets["view"].model() - model = model.sourceModel() + model_ = self._widgets["view"].model() + model_ = model_.sourceModel() - package_count = model.rowCount() - override_count = len([i for i in model.items if i["override"]]) - disabled_count = len([i for i in model.items if i["disabled"]]) + package_count = model_.rowCount() + override_count = len([i for i in model_.items if i["override"]]) + disabled_count = len([i for i in model_.items if i["disabled"]]) self._widgets["status"].showMessage( "%d Packages, %d Overridden, %d Disabled" % ( @@ -662,7 +667,7 @@ def on_print_code_clicked(self): self._widgets["code"].setText("
".join(pretty)) - def on_application_changed(self): + def on_application_changed(self, app_request): self._widgets["code"].setPlainText("") if not self._widgets["graph"]._pixmapHandle: return diff --git a/allzpark/model.py b/allzpark/model.py index e989df8..1f26aec 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -163,7 +163,7 @@ def parse_icon(root, template): return QtGui.QIcon(fname) -class ApplicationModel(AbstractTableModel): +class ResolvedPackagesModel(AbstractTableModel): ColumnToKey = { 0: { QtCore.Qt.DisplayRole: "label", @@ -171,55 +171,95 @@ class ApplicationModel(AbstractTableModel): }, 1: { QtCore.Qt.DisplayRole: "version", - } + }, + 2: { + QtCore.Qt.DisplayRole: "state", + }, + 3: { + QtCore.Qt.DisplayRole: "latest", + }, + 4: { + QtCore.Qt.DisplayRole: "beta", + }, } - Headers = [ - "application", - "version" - ] - def __init__(self, *args, **kwargs): - super(ApplicationModel, self).__init__(*args, **kwargs) + super(ResolvedPackagesModel, self).__init__(*args, **kwargs) self._broken_icon = res.icon("Action_Stop_1_32.png") + self._overrides = {} + self._disabled = {} + + @property + def overrides(self): + return self._overrides + + @property + def disabled(self): + return self._disabled - def reset(self, applications=None): - applications = applications or [] + def reset(self, packages=None): + packages = packages or dict() self.beginResetModel() self.items[:] = [] - for app in applications: - root = app.root + for app_request, app_data in packages.items(): + app = app_data["app"] + versions = app_data["versions"] - data = allzparkconfig.metadata_from_package(app) - tools = getattr(app, "tools", None) or [app.name] - app_request = "%s==%s" % (app.name, app.version) + self._add_item(app_request, app_request, app, versions) - item = { - "name": app_request, - "label": data["label"], - "version": str(app.version), - "icon": parse_icon(root, template=data["icon"]), - "package": app, - "context": None, - "active": True, - "hidden": data["hidden"], - "broken": isinstance(app, BrokenPackage), + for pkg in app_data["packages"]: + self._add_item(pkg.name, app_request, pkg) - # Whether or not to open a separate console for this app - "detached": False, + self.endResetModel() - # Current tool - "tool": None, + def _add_item(self, name, app_request, pkg, versions=None): + root = pkg.root + is_app = versions is not None + data = allzparkconfig.metadata_from_package(pkg) + tools = getattr(pkg, "tools", None) or [pkg.name] + version = str(pkg.version) + relocatable = localz.is_relocatable(pkg) if localz else False + state = ( + "(dev)" if is_local(pkg) else + "(localised)" if is_localised(pkg) else + "" + ) - # All available tools - "tools": tools, - } + item = { + "_isApp": is_app, - self.items.append(item) + "name": name, + "label": data["label"], + "version": version, + "versions": versions or [version], + "icon": parse_icon(root, template=data["icon"]), + "package": pkg, + "context": None, + "active": True, + + "family": pkg.name, + "request": app_request, + "hidden": data["hidden"], + "broken": isinstance(pkg, BrokenPackage), + + "default": version, + "override": self._overrides.get(pkg.name), + "disabled": self._disabled.get(pkg.name, False), + "state": state, + "relocatable": relocatable, + "localizing": False, # in progress + + # Whether or not to open a separate console for this app + "detached": False, + # Current tool + "tool": None, + # All available tools + "tools": tools, + } - self.endResetModel() + self.items.append(item) def data(self, index, role): row = index.row() @@ -251,174 +291,6 @@ def data(self, index, role): if col == 0: return self._broken_icon - return super(ApplicationModel, self).data(index, role) - - -class BrokenContext(object): - broken_dict = {"error": "Failed context"} - - def __init__(self, app_name, request): - self.resolved_packages = [BrokenPackage(app_name)] - self.success = False - self.timestamp = 0 - - self._request = request - - def requested_packages(self): - return self._request - - def to_dict(self, *args, **kwargs): - return self.broken_dict - - def get_environ(self, *args, **kwargs): - raise rez.ResolvedContextError("This is a broken context.") - - -class BrokenPackage(object): - def __str__(self): - return self.name - - def __init__(self, request): - request = rez.PackageRequest(request) - versions = request.range.to_versions() or [None] - - self.name = request.name - self.version = versions[-1] - self.qualified_name = "%s-%s" % (self.name, str(self.version)) - self.uri = "" - self.root = "" - self.relocatable = False - self.requires = [] - self.resource = type( - "BrokenResource", (object,), {"repository_type": None} - )() - - self._data = { - "label": request.name, - } - - -def is_local(pkg): - if pkg.resource.repository_type != "filesystem": - return False - - local_path = rez.config.local_packages_path - local_path = os.path.abspath(local_path) - local_path = os.path.normpath(local_path) - - pkg_path = pkg.resource.location - pkg_path = os.path.abspath(pkg_path) - pkg_path = os.path.normpath(pkg_path) - - return pkg_path.startswith(local_path) - - -def is_localised(pkg): - if localz: - root = util.normpath(pkg.root) - path = util.normpath(localz.localized_packages_path()) - return root.startswith(path) - else: - return False - - -class PackagesModel(AbstractTableModel): - ColumnToKey = { - 0: { - QtCore.Qt.DisplayRole: "label", - QtCore.Qt.DecorationRole: "icon", - }, - 1: { - QtCore.Qt.DisplayRole: "version", - }, - 2: { - QtCore.Qt.DisplayRole: "state", - }, - 3: { - QtCore.Qt.DisplayRole: "latest", - }, - 4: { - QtCore.Qt.DisplayRole: "beta", - }, - } - - Headers = [ - "package", - "version", - "state", - "latest", - "beta", - ] - - def __init__(self, ctrl, parent=None): - super(PackagesModel, self).__init__(parent) - - self._ctrl = ctrl - self._overrides = {} - self._disabled = {} - - def reset(self, packages=None): - packages = packages or [] - - self.beginResetModel() - self.items[:] = [] - - # TODO: This isn't nice. The model should - # not have to reach into the controller. - paths = self._ctrl._package_paths() - - for pkg in packages: - root = pkg.root - data = allzparkconfig.metadata_from_package(pkg) - state = ( - "(dev)" if is_local(pkg) else - "(localised)" if is_localised(pkg) else - "" - ) - relocatable = False - - version = str(pkg.version) - - # Fetch all versions of package - versions = rez.find(pkg.name, paths=paths) - versions = sorted( - [str(v.version) for v in versions], - key=util.natural_keys - ) or [version] # broken package - - if localz: - relocatable = localz.is_relocatable(pkg) - - item = { - "name": pkg.name, - "label": data["label"], - "version": version, - "default": version, - "icon": parse_icon(root, template=data["icon"]), - "package": pkg, - "override": self._overrides.get(pkg.name), - "disabled": self._disabled.get(pkg.name, False), - "context": None, - "active": True, - "versions": versions, - "state": state, - "relocatable": relocatable, - "localizing": False, # in progress - } - - self.items.append(item) - - self.endResetModel() - - def data(self, index, role): - row = index.row() - col = index.column() - - try: - data = self.items[row] - except IndexError: - return None - if data["override"]: if role == QtCore.Qt.DisplayRole and col == 1: return data["override"] @@ -442,7 +314,13 @@ def data(self, index, role): return QtGui.QColor("darkorange") try: - return data[role] + value = data[role] + + if isinstance(value, list): + # Prevent edits + value = value[:] + + return value except KeyError: try: @@ -455,6 +333,7 @@ def data(self, index, role): return "x" if re.findall(r".beta$", version) else "" if key == "latest": + # TODO: this is not the real latest version = data["override"] or data["version"] latest = data["versions"][-1] return "x" if version == latest else "" @@ -485,7 +364,7 @@ def setData(self, index, value, role): self._disabled[package] = value - return super(PackagesModel, self).setData(index, value, role) + return super(ResolvedPackagesModel, self).setData(index, value, role) def flags(self, index): if index.column() == 1: @@ -495,7 +374,75 @@ def flags(self, index): QtCore.Qt.ItemIsEditable ) - return super(PackagesModel, self).flags(index) + return super(ResolvedPackagesModel, self).flags(index) + + +class BrokenContext(object): + broken_dict = {"error": "Failed context"} + + def __init__(self, app_name, request): + self.resolved_packages = [BrokenPackage(app_name)] + self.success = False + self.timestamp = 0 + + self._request = request + + def requested_packages(self): + return self._request + + def to_dict(self, *args, **kwargs): + return self.broken_dict + + def get_environ(self, *args, **kwargs): + raise rez.ResolvedContextError("This is a broken context.") + + +class BrokenPackage(object): + def __str__(self): + return self.name + + def __init__(self, request): + request = rez.PackageRequest(request) + versions = request.range.to_versions() or [None] + + self.name = request.name + self.version = versions[-1] + self.qualified_name = "%s-%s" % (self.name, str(self.version)) + self.uri = "" + self.root = "" + self.relocatable = False + self.requires = [] + self.resource = type( + "BrokenResource", (object,), {"repository_type": None} + )() + + self._data = { + "label": request.name, + } + + +def is_local(pkg): + if pkg.resource.repository_type != "filesystem": + return False + + local_path = rez.config.local_packages_path + local_path = os.path.abspath(local_path) + local_path = os.path.normpath(local_path) + + pkg_path = pkg.resource.location + pkg_path = os.path.abspath(pkg_path) + pkg_path = os.path.normpath(pkg_path) + + return pkg_path.startswith(local_path) + + +def is_localised(pkg): + if localz: + root = util.normpath(pkg.root) + path = util.normpath(localz.localized_packages_path()) + return root.startswith(path) + else: + return False class CommandsModel(AbstractTableModel): @@ -684,6 +631,42 @@ def rowCount(self, parent=QtCore.QModelIndex()): return super(ProxyModel, self).rowCount(parent) +class ApplicationProxyModel(ProxyModel): + + Headers = [ + "application", + "version" + ] + + def columnCount(self, parent): + return len(self.Headers) + + def headerData(self, section, orientation, role): + if orientation == QtCore.Qt.Vertical: + return + + if role == QtCore.Qt.DisplayRole: + return self.Headers[section] + + +class PackagesProxyModel(ProxyModel): + + Headers = [ + "package", + "version", + "state", + "latest", + "beta", + ] + + def headerData(self, section, orientation, role): + if orientation == QtCore.Qt.Vertical: + return + + if role == QtCore.Qt.DisplayRole: + return self.Headers[section] + + class TreeItem(dict): def __init__(self, data=None): diff --git a/allzpark/view.py b/allzpark/view.py index 17f1112..dc3e5a9 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -11,15 +11,20 @@ from .vendor import qargparse from .version import version from . import resources as res, dock, model -from . import allzparkconfig +from . import allzparkconfig, delegates px = res.px class Applications(dock.SlimTableView): - def __init__(self, parent=None): + def __init__(self, ctrl, parent=None): super(Applications, self).__init__(parent) + delegate = delegates.Package(ctrl, self) + self.setItemDelegate(delegate) + self.setEditTriggers(self.EditKeyPressed) + self.setStretch(0) + self._selected_app_ok = False def on_state_appfailed(self): @@ -31,6 +36,13 @@ def on_state_appok(self): def is_selected_app_ok(self): return self._selected_app_ok + def setModel(self, model_): + proxy_model = model.ApplicationProxyModel(model_) + proxy_model.setup(include=[ + ("_isApp", True) + ]) + super(Applications, self).setModel(proxy_model) + class Window(QtWidgets.QMainWindow): title = "Allzpark %s" % version @@ -76,7 +88,7 @@ def __init__(self, ctrl, parent=None): "logo": QtWidgets.QToolButton(), "appVersion": QtWidgets.QLabel(version), - "apps": Applications(), + "apps": Applications(ctrl), "fullCommand": FullCommand(ctrl), # Error page @@ -264,15 +276,14 @@ def on_visible(widget, toggle, state): docks["profiles"].set_model(ctrl.models["profiles"], ctrl.models["profileVersions"]) - docks["packages"].set_model(ctrl.models["packages"]) + docks["packages"].set_model(ctrl.models["resolved"]) docks["context"].set_model(ctrl.models["context"]) docks["environment"].set_model(ctrl.models["environment"], ctrl.models["parentenv"], ctrl.models["diagnose"]) docks["commands"].set_model(ctrl.models["commands"]) - proxy_model = model.ProxyModel(ctrl.models["apps"]) - widgets["apps"].setModel(proxy_model) + widgets["apps"].setModel(ctrl.models["resolved"]) widgets["errorMessage"].setAlignment(QtCore.Qt.AlignHCenter) @@ -295,7 +306,7 @@ def on_visible(widget, toggle, state): selection_model = widgets["apps"].selectionModel() selection_model.selectionChanged.connect(self.on_app_selection_changed) - ctrl.models["apps"].modelReset.connect(self.on_apps_reset) + ctrl.models["resolved"].modelReset.connect(self.on_apps_reset) ctrl.models["profiles"].modelReset.connect( self.on_profilename_reset) ctrl.models["profileVersions"].modelReset.connect( @@ -308,6 +319,7 @@ def on_visible(widget, toggle, state): ctrl.repository_changed.connect(self.on_repository_changed) ctrl.command_changed.connect(self.on_command_changed) ctrl.application_changed.connect(self.on_app_changed) + ctrl.application_changed.connect(docks["packages"].on_app_changed) self._pages = pages self._widgets = widgets @@ -702,7 +714,7 @@ def on_apps_reset(self): app = self._ctrl.state.retrieve("startupApplication") row = 0 - model = self._ctrl.models["apps"] + model = self._ctrl.models["resolved"] if app: for row_ in range(model.rowCount()): @@ -723,6 +735,9 @@ def on_app_clicked(self, index): app.show() self.on_dock_toggled(app, visible=True) + if index.column() == 1: + self._widgets["apps"].edit(index) + def on_app_selection_changed(self, selected, deselected): """The current app was changed @@ -742,7 +757,7 @@ def on_app_selection_changed(self, selected, deselected): app_request = model.data(index, "name") self._ctrl.select_application(app_request) - def on_app_changed(self): + def on_app_changed(self, app_request): selection_model = self._widgets["apps"].selectionModel() index = selection_model.selectedIndexes()[0] self._docks["app"].refresh(index) From a6db0f40871c2af8a03db0f26f2f696bd945c407 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 00:05:58 +0800 Subject: [PATCH 02/28] block/unblock app selecting on version picking --- allzpark/delegates.py | 20 ++++++++++++++++++++ allzpark/view.py | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/allzpark/delegates.py b/allzpark/delegates.py index e1d2202..2352568 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -2,8 +2,19 @@ class Package(QtWidgets.QStyledItemDelegate): + + editor_created = QtCore.Signal() + editor_closed = QtCore.Signal(bool) + def __init__(self, ctrl, parent=None): super(Package, self).__init__(parent) + + def on_close_editor(*args): + self.editor_closed.emit(self._changed) + self.closeEditor.connect(on_close_editor) + + self._changed = None + self._default = None self._ctrl = ctrl def createEditor(self, parent, option, index): @@ -12,6 +23,10 @@ def createEditor(self, parent, option, index): editor = QtWidgets.QComboBox(parent) + def on_text_activated(text): + self._changed = text != self._default + editor.textActivated.connect(on_text_activated) + return editor def setEditorData(self, editor, index): @@ -19,9 +34,14 @@ def setEditorData(self, editor, index): options = model.data(index, "versions") default = index.data(QtCore.Qt.DisplayRole) + self._changed = False + self._default = default + editor.addItems(options) editor.setCurrentIndex(options.index(default)) + self.editor_created.emit() + def setModelData(self, editor, model, index): model = index.model() package = model.data(index, "family") diff --git a/allzpark/view.py b/allzpark/view.py index dc3e5a9..53bf154 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -25,8 +25,31 @@ def __init__(self, ctrl, parent=None): self.setEditTriggers(self.EditKeyPressed) self.setStretch(0) + # Block/Unblock view selection signal on version picking + # - + # We are using combobox widget as package version delegate editor, + # when user done picking version, may clicking on any place out + # side of the combobox widget instead of pressing return button to + # trigger editing finished signal. If user is clicking on application + # view, the application changed signal will also being emitted at the + # same time while the version editor already called context patching, + # race condition happened. + # To avoid that, we need to block view selection signal when editor + # is created, and unblock it depend on version changed or not. + # If version isn't changed, unblock it once editor is closed, and + # wait for controller reset signal after patch completed if changed. + delegate.editor_created.connect(self.on_editor_created) + delegate.editor_closed.connect(self.on_editor_done) + ctrl.resetted.connect(lambda: self.on_editor_done(False)) + self._selected_app_ok = False + def on_editor_created(self): + self.selectionModel().blockSignals(True) + + def on_editor_done(self, block): + self.selectionModel().blockSignals(block) + def on_state_appfailed(self): self._selected_app_ok = False From 0e4b652a2ba168b895415c68c9718d7c2fdaa29b Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 00:06:30 +0800 Subject: [PATCH 03/28] cosmetic --- allzpark/dock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/allzpark/dock.py b/allzpark/dock.py index c0c440d..d1cbf0b 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -343,15 +343,15 @@ def __init__(self, ctrl, parent=None): def on_argument_changed(self, arg): if arg["name"] == "useDevelopmentPackages": - self._ctrl._state.store("useDevelopmentPackages", arg.read()) + self._ctrl.state.store("useDevelopmentPackages", arg.read()) self._ctrl.reset() if arg["name"] == "useLocalizedPackages": - self._ctrl._state.store("useLocalizedPackages", arg.read()) + self._ctrl.state.store("useLocalizedPackages", arg.read()) self._ctrl.reset() if arg["name"] == "patch": - self._ctrl._state.store("patch", arg.read()) + self._ctrl.state.store("patch", arg.read()) self._ctrl.reset() def on_resetted(self): From 0a85955b8fcd77249073291cc44790e633da6d10 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 00:11:45 +0800 Subject: [PATCH 04/28] add todo notes --- allzpark/control.py | 1 + allzpark/dock.py | 3 +++ allzpark/model.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/allzpark/control.py b/allzpark/control.py index 4fd0344..0781b0b 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -919,6 +919,7 @@ def on_apps_not_found(error, trace): active_profile = profile_versions[version_name] if profile_name: + # (TODO): this warning pops-up even profile actually exists self.warning("%s was not found" % profile_name) else: self.error("select_profile was passed an empty string") diff --git a/allzpark/dock.py b/allzpark/dock.py index d1cbf0b..5ac0135 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -351,6 +351,9 @@ def on_argument_changed(self, arg): self._ctrl.reset() if arg["name"] == "patch": + # (TODO) This will be called twice since qargparse.String + # may emit changed signal twice. And profile model item + # will get doubled. self._ctrl.state.store("patch", arg.read()) self._ctrl.reset() diff --git a/allzpark/model.py b/allzpark/model.py index 1f26aec..d10b6e3 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -333,7 +333,7 @@ def data(self, index, role): return "x" if re.findall(r".beta$", version) else "" if key == "latest": - # TODO: this is not the real latest + # (TODO): This is not the real latest version = data["override"] or data["version"] latest = data["versions"][-1] return "x" if version == latest else "" From eab9aca32d506744264112e4f38d01687c76c413 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 00:15:42 +0800 Subject: [PATCH 05/28] Fix iter NoneType --- allzpark/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allzpark/control.py b/allzpark/control.py index 0781b0b..35c1f62 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1216,7 +1216,7 @@ def _try_resolve_context(req, pkg_name, mode): key=util.natural_keys ) resolved_packages = [ - pkg for pkg in contexts[request].resolved_packages + pkg for pkg in contexts[request].resolved_packages or [] if pkg.name != package.name ] visible_apps[request] = { From b422611ab5c57df3da98746c46a450009ad0fecb Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 18:41:28 +0800 Subject: [PATCH 06/28] reduce package finding call --- allzpark/control.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 35c1f62..dbd2392 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1093,12 +1093,19 @@ def _list_apps(self, profile): def _try_finding_latest_app(req_str): req_str = req_str.strip("~") req = rez.PackageRequest(req_str) - app_ranges[req.name] = req.range try: - return rez.find_latest(req.name, range_=req.range) + app_vers = list(self.find(req.name, range_=req.range)) + latest = app_vers[-1] + except IndexError: + self.error("No package matched for request '%s', may have" + "been excluded by package filter.") + return model.BrokenPackage(req_str) except _missing as e_: self.error(str(e_)) return model.BrokenPackage(req_str) + else: + app_ranges[req.name] = app_vers + return latest def _try_resolve_context(req, pkg_name, mode): kwargs = dict() @@ -1199,7 +1206,6 @@ def _try_resolve_context(req, pkg_name, mode): # * Opt-out hidden application # * Find application versions # * Context resolved packages (latest only) - paths = self._package_paths() show_hidden = self._state.retrieve("showHiddenApps") for request, package in self._state["rezApps"].items(): data = allzparkconfig.metadata_from_package(package) @@ -1208,13 +1214,7 @@ def _try_resolve_context(req, pkg_name, mode): if hidden and not show_hidden: continue - versions = rez.find(package.name, - range_=app_ranges[package.name], - paths=paths) - versions = sorted( - [str(v.version) for v in versions], - key=util.natural_keys - ) + versions = [str(v.version) for v in app_ranges[package.name]] resolved_packages = [ pkg for pkg in contexts[request].resolved_packages or [] if pkg.name != package.name From 626cdf8458308ac90a096817fe1fc4970d2a7b88 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 23:48:17 +0800 Subject: [PATCH 07/28] implement showAllVersions Add a preference setting that will show all package versions. Profile requested application version range will still be respected, but all versions of each dependency package will be shown. --- allzpark/control.py | 36 ++++++++++++++++++++++-------------- allzpark/dock.py | 7 ++++++- allzpark/model.py | 15 +++++++-------- allzpark/view.py | 1 + 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index dbd2392..b17b91b 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1099,13 +1099,15 @@ def _try_finding_latest_app(req_str): except IndexError: self.error("No package matched for request '%s', may have" "been excluded by package filter.") - return model.BrokenPackage(req_str) + latest = model.BrokenPackage(req_str) + app_vers = [latest] except _missing as e_: self.error(str(e_)) - return model.BrokenPackage(req_str) - else: - app_ranges[req.name] = app_vers - return latest + latest = model.BrokenPackage(req_str) + app_vers = [latest] + + app_ranges[req.name] = app_vers + return latest def _try_resolve_context(req, pkg_name, mode): kwargs = dict() @@ -1206,22 +1208,28 @@ def _try_resolve_context(req, pkg_name, mode): # * Opt-out hidden application # * Find application versions # * Context resolved packages (latest only) + all_vers = self._state.retrieve("showAllVersions", False) show_hidden = self._state.retrieve("showHiddenApps") - for request, package in self._state["rezApps"].items(): - data = allzparkconfig.metadata_from_package(package) + for request, app_pkg in self._state["rezApps"].items(): + data = allzparkconfig.metadata_from_package(app_pkg) hidden = data.get("hidden", False) if hidden and not show_hidden: continue - versions = [str(v.version) for v in app_ranges[package.name]] - resolved_packages = [ - pkg for pkg in contexts[request].resolved_packages or [] - if pkg.name != package.name - ] + app_versions = [str(v.version) for v in app_ranges[app_pkg.name]] + resolved_packages = { + pkg: [str(p.version) + for p in (self.find(pkg.name) if all_vers else [pkg])] + for pkg in contexts[request].resolved_packages or [] + if pkg.name not in [app_pkg.name, profile_variant.name] + } + # profile version should not be changed in Packages view + resolved_packages[profile_variant] = [profile_variant] + visible_apps[request] = { - "app": package, - "versions": versions, + "app": app_pkg, + "versions": app_versions, "packages": resolved_packages, } diff --git a/allzpark/dock.py b/allzpark/dock.py index 5ac0135..f703452 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -1093,7 +1093,12 @@ class Preferences(AbstractDockWidget): qargparse.Boolean("showHiddenApps", help=( "Show apps with metadata['hidden'] = True" )), - + qargparse.Boolean("showAllVersions", help=( + "Show all package versions.\n" + "Profile requested application version range will still be \n" + "respected, but all versions of each dependency package will \n" + "be shown." + )), qargparse.Boolean("patchWithFilter", help=( "Use the current exclusion filter when patching.\n" "This enables patching of packages outside of a filter, \n" diff --git a/allzpark/model.py b/allzpark/model.py index d10b6e3..f1574c3 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -205,18 +205,18 @@ def reset(self, packages=None): for app_request, app_data in packages.items(): app = app_data["app"] + name = app_request versions = app_data["versions"] + self._add_item(name, app_request, app, versions, is_app=True) - self._add_item(app_request, app_request, app, versions) - - for pkg in app_data["packages"]: - self._add_item(pkg.name, app_request, pkg) + for pkg, versions in app_data["packages"].items(): + name = pkg.name + self._add_item(name, app_request, pkg, versions) self.endResetModel() - def _add_item(self, name, app_request, pkg, versions=None): + def _add_item(self, name, app_request, pkg, versions, is_app=False): root = pkg.root - is_app = versions is not None data = allzparkconfig.metadata_from_package(pkg) tools = getattr(pkg, "tools", None) or [pkg.name] version = str(pkg.version) @@ -233,7 +233,7 @@ def _add_item(self, name, app_request, pkg, versions=None): "name": name, "label": data["label"], "version": version, - "versions": versions or [version], + "versions": versions, "icon": parse_icon(root, template=data["icon"]), "package": pkg, "context": None, @@ -333,7 +333,6 @@ def data(self, index, role): return "x" if re.findall(r".beta$", version) else "" if key == "latest": - # (TODO): This is not the real latest version = data["override"] or data["version"] latest = data["versions"][-1] return "x" if version == latest else "" diff --git a/allzpark/view.py b/allzpark/view.py index 53bf154..e93a455 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -507,6 +507,7 @@ def on_setting_changed(self, argument): if key in ("showAllApps", "showHiddenApps", + "showAllVersions", "patchWithFilter"): self._ctrl.reset() From 24d40fe07774d430a2cebc34e08c3b85bc7b76ac Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 23:54:44 +0800 Subject: [PATCH 08/28] distinguish version changeable package --- allzpark/delegates.py | 3 ++- allzpark/dock.py | 13 ++++++++++--- allzpark/model.py | 7 +++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/allzpark/delegates.py b/allzpark/delegates.py index 2352568..b72502a 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -18,7 +18,8 @@ def on_close_editor(*args): self._ctrl = ctrl def createEditor(self, parent, option, index): - if index.column() != 1: + model = index.model() + if index.column() != 1 or not model.data(index, "_hasVersions"): return editor = QtWidgets.QComboBox(parent) diff --git a/allzpark/dock.py b/allzpark/dock.py index f703452..44d6d6c 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -465,8 +465,17 @@ def on_right_click(self, position): localize_all.setEnabled(False) localize_related.setEnabled(False) + versions = model_.data(index, "versions") + if len(versions) <= 1: + edit.setEnabled(False) + default.setEnabled(False) + earliest.setEnabled(False) + latest.setEnabled(False) + def on_edit(): - self._widgets["view"].edit(index) + # avoid sending index that is not editable + version_index = model_.index(index.row(), 1) + self._widgets["view"].edit(version_index) def on_default(): package = model_.data(index, "package") @@ -474,7 +483,6 @@ def on_default(): self.message.emit("Package set to default") def on_earliest(): - versions = model_.data(index, "versions") earliest = versions[0] package = model_.data(index, "package") self._ctrl.patch("%s==%s" % (package.name, earliest)) @@ -482,7 +490,6 @@ def on_earliest(): self.message.emit("Package set to earliest") def on_latest(): - versions = model_.data(index, "versions") latest = versions[-1] package = model_.data(index, "package") self._ctrl.patch("%s==%s" % (package.name, latest)) diff --git a/allzpark/model.py b/allzpark/model.py index f1574c3..3ae7547 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -229,6 +229,7 @@ def _add_item(self, name, app_request, pkg, versions, is_app=False): item = { "_isApp": is_app, + "_hasVersions": len(versions) > 1, "name": name, "label": data["label"], @@ -313,6 +314,12 @@ def data(self, index, role): if role == QtCore.Qt.ForegroundRole: return QtGui.QColor("darkorange") + if data["_hasVersions"] and col == 1: + if role == QtCore.Qt.FontRole: + font = QtGui.QFont() + font.setBold(True) + return font + try: value = data[role] From 3d2e5c4fc9efdb9f218f74b87e1c58f288db08bd Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 12 Nov 2020 23:56:14 +0800 Subject: [PATCH 09/28] implement preference protection Allow admin to block preference from user with config --- allzpark/allzparkconfig.py | 23 +++++++++++++++++++++++ allzpark/dock.py | 9 +++++++++ 2 files changed, 32 insertions(+) diff --git a/allzpark/allzparkconfig.py b/allzpark/allzparkconfig.py index f5a74cc..0c459f0 100644 --- a/allzpark/allzparkconfig.py +++ b/allzpark/allzparkconfig.py @@ -103,6 +103,29 @@ def metadata_from_package(variant): }) +def protected_preferences(): + """Protect preference settings + + Prevent clueless one from touching danger settings. + + Following is a list of preference names that you may lock: + * showAllApps (bool) + * showHiddenApps (bool) + * showAllVersions (bool) + * patchWithFilter (bool) + * clearCacheTimeout (int) + * exclusionFilter (str) + + This should return a preference name and default value paired + dict. For example: {"showAllVersions": False} + + Returns: + dict + + """ + return dict() + + def themes(): """Allzpark GUI theme list provider diff --git a/allzpark/dock.py b/allzpark/dock.py index 44d6d6c..b6ca0b5 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -1155,6 +1155,15 @@ def __init__(self, window, ctrl, parent=None): self.setAttribute(QtCore.Qt.WA_StyledBackground) self.setObjectName("Preferences") + protected = allzparkconfig.protected_preferences() + for name, value in protected.items(): + arg = next((a for a in self.options if a["name"] == name), None) + if arg is None: + print("Unknown preference setting: %s" % name) + else: + ctrl.state.store(name, value) + arg["enabled"] = False + panels = { "central": QtWidgets.QTabWidget(), } From bb23c22668d8a6184e01964fe0d094471d5b83c7 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 13 Nov 2020 00:49:22 +0800 Subject: [PATCH 10/28] block context graphing only on package missing --- allzpark/control.py | 7 +++++++ allzpark/dock.py | 5 +++-- allzpark/view.py | 3 ++- tests/test_docks.py | 3 --- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index b17b91b..1e1ed5d 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -205,6 +205,7 @@ class Controller(QtCore.QObject): _State("resolving", help="Rez is busy resolving a context"), _State("loading", help="Something is taking a moment"), _State("errored", help="Something has gone wrong"), + _State("console", help="Something need you to read"), _State("launching", help="An application is launching"), _State("ready", help="Awaiting user input"), _State("noprofiles", help="Allzpark did not find any profiles at all"), @@ -1237,6 +1238,12 @@ def _try_resolve_context(req, pkg_name, mode): 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 + graph_str = context.graph(as_dot=True) tempdir = tempfile.mkdtemp() diff --git a/allzpark/dock.py b/allzpark/dock.py index b6ca0b5..2f8fbac 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -640,16 +640,17 @@ def set_model(self, model_): self._model = model_ def on_state_appfailed(self): - self._widgets["generateGraph"].setEnabled(False) self._widgets["printCode"].setEnabled(False) def on_state_appok(self): - self._widgets["generateGraph"].setEnabled(True) self._widgets["printCode"].setEnabled(True) def on_generate_clicked(self): pixmap = self._ctrl.graph() + if pixmap is None: + return # was graphing broken context + if not pixmap: self._widgets["graphHotkeys"].setText( "GraphViz not found" diff --git a/allzpark/view.py b/allzpark/view.py index e93a455..9a64146 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -699,11 +699,12 @@ def on_state_changed(self, state): for widget in self._docks.values(): widget.setEnabled(False) - if state in ("pkgnotfound", "errored"): + if state in ("pkgnotfound", "errored", "console"): console = self._docks["console"] console.show() self.on_dock_toggled(console, visible=True) + if state in ("pkgnotfound", "errored"): page = self._pages["errored"] self._panels["pages"].setCurrentWidget(page) self._widgets["apps"].setEnabled(False) diff --git a/tests/test_docks.py b/tests/test_docks.py index bc5826a..e3d0620 100644 --- a/tests/test_docks.py +++ b/tests/test_docks.py @@ -39,9 +39,6 @@ def test_feature_blocked_on_failed_app(self): dock = self.show_dock("environment", on_page="diagnose") self.assertEqual(dock._widgets["compute"].isEnabled(), state) - dock = self.show_dock("context", on_page="graph") - self.assertEqual(dock._widgets["generateGraph"].isEnabled(), state) - dock = self.show_dock("context", on_page="code") self.assertEqual(dock._widgets["printCode"].isEnabled(), state) From cac27ba32b2ae3aabafe84e55ec8c6d3aeab8690 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 13 Nov 2020 23:39:35 +0800 Subject: [PATCH 11/28] revert model merge --- allzpark/control.py | 62 ++++--- allzpark/dock.py | 11 +- allzpark/model.py | 412 ++++++++++++++++++++++++-------------------- allzpark/view.py | 19 +- 4 files changed, 268 insertions(+), 236 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 1e1ed5d..1e2a31b 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -191,7 +191,7 @@ class Controller(QtCore.QObject): profile_changed = QtCore.Signal( str, object, bool) # profile, version, refreshed - application_changed = QtCore.Signal(str) + application_changed = QtCore.Signal() # The current command to launch an application has changed command_changed = QtCore.Signal(str) # command @@ -228,11 +228,12 @@ def __init__(self, state = State(self, storage, parent_environ) models = { - "resolved": model.ResolvedPackagesModel(), + "apps": model.ApplicationModel(), "profileVersions": QtCore.QStringListModel(), # Docks "profiles": model.ProfileModel(), + "packages": model.PackagesModel(), "context": model.ContextModel(), "environment": model.EnvironmentModel(), "parentenv": model.EnvironmentModel(), @@ -354,6 +355,25 @@ def environ(self, app_request): env[app_request] = environ return environ + def resolved_packages(self, app_request): + all_vers = self._state.retrieve("showAllVersions", False) + profile_name = self._state["profileName"] + resolved = self._state["rezContexts"][app_request].resolved_packages + packages = odict() # keep resolved order + for pkg in resolved or []: + # profile version should not be changed in Packages view + _all_vers = pkg.name != profile_name + versions = [ + str(p.version) + for p in (self.find(pkg.name) if all_vers else [pkg]) + ] + packages[pkg.name] = { + "package": pkg, + "versions": versions, + } + + return packages + # ---------------- # Events # ---------------- @@ -716,7 +736,7 @@ def do(): rez_app.name, rez_app.version )) - app_model = self._models["resolved"] + app_model = self._models["apps"] app_index = app_model.findIndex(app_request) tool_name = kwargs.get( @@ -731,8 +751,8 @@ def do(): "This is a bug" ) - overrides = self._models["resolved"].overrides - disabled = self._models["resolved"].disabled + overrides = self._models["packages"].overrides + disabled = self._models["packages"].disabled environ = self.parent_environ() self.debug( @@ -807,7 +827,7 @@ def on_failure(error, trace): def delocalize(self, name): def do(): - item = self._models["resolved"].find(name) + item = self._models["packages"].find(name) package = item["package"] self.debug("Delocalizing %s" % package.root) localz.delocalize(package) @@ -869,10 +889,11 @@ def list_profiles(self, root=None): def select_profile(self, profile_name, version_name=Latest): # Wipe existing data - self._models["resolved"].reset() + self._models["apps"].reset() self._models["context"].reset() self._models["environment"].reset() self._models["diagnose"].reset() + self._models["packages"].reset() self._models["profileVersions"].setStringList([]) self._state["rezContexts"].clear() @@ -880,8 +901,8 @@ def select_profile(self, profile_name, version_name=Latest): self._state["testedEnvirons"].clear() self._state["rezApps"].clear() - def on_apps_found(packages): - if not packages: + def on_apps_found(apps): + if not apps: self._state["error"] = """

:(


@@ -901,7 +922,7 @@ def on_apps_found(packages): self._state.to_noapps() else: - self._models["resolved"].reset(packages) + self._models["apps"].reset(apps) self._state.to_ready() def on_apps_not_found(error, trace): @@ -965,25 +986,28 @@ def select_application(self, app_request): try: context = self.context(app_request) environ = self.environ(app_request) + packages = self.resolved_packages(app_request) diagnose = self._state["testedEnvirons"].get(app_request, {}) except Exception: + self._models["packages"].reset() self._models["context"].reset() self._models["environment"].reset() self._models["diagnose"].reset() raise + self._models["packages"].reset(packages) self._models["context"].load(context.to_dict()) self._models["environment"].load(environ) self._models["diagnose"].load(diagnose) - tools = self._models["resolved"].find(app_request)["tools"] + tools = self._models["apps"].find(app_request)["tools"] self._state["tool"] = tools[0] # Use this application on next launch or change of profile self.update_command() self._state.store("startupApplication", app_request) - self.application_changed.emit(app_request) + self.application_changed.emit() if context.success: self._state.to_appok() @@ -1208,8 +1232,6 @@ def _try_resolve_context(req, pkg_name, mode): # * Opt-out hidden application # * Find application versions - # * Context resolved packages (latest only) - all_vers = self._state.retrieve("showAllVersions", False) show_hidden = self._state.retrieve("showHiddenApps") for request, app_pkg in self._state["rezApps"].items(): data = allzparkconfig.metadata_from_package(app_pkg) @@ -1219,19 +1241,9 @@ def _try_resolve_context(req, pkg_name, mode): continue app_versions = [str(v.version) for v in app_ranges[app_pkg.name]] - resolved_packages = { - pkg: [str(p.version) - for p in (self.find(pkg.name) if all_vers else [pkg])] - for pkg in contexts[request].resolved_packages or [] - if pkg.name not in [app_pkg.name, profile_variant.name] - } - # profile version should not be changed in Packages view - resolved_packages[profile_variant] = [profile_variant] - visible_apps[request] = { - "app": app_pkg, + "package": app_pkg, "versions": app_versions, - "packages": resolved_packages, } return visible_apps diff --git a/allzpark/dock.py b/allzpark/dock.py index 2f8fbac..d06e23d 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -174,7 +174,7 @@ def on_arg_changed(self, arg): return ctrl = self._ctrl - model = ctrl.models["resolved"] + model = ctrl.models["apps"] app_name = ctrl.state["appRequest"] app_index = model.findIndex(app_name) value = arg.read() @@ -364,17 +364,12 @@ def on_resetted(self): arg._previous = patch def set_model(self, model_): - proxy_model = model.PackagesProxyModel(model_) - proxy_model.setup(include=[("request", None)]) + proxy_model = model.ProxyModel(model_) self._widgets["view"].setModel(proxy_model) model_.modelReset.connect(self.on_model_changed) model_.dataChanged.connect(self.on_model_changed) - def on_app_changed(self, app_request): - model_ = self._widgets["view"].model() - model_.setup(include=[("request", app_request)]) - def on_model_changed(self): model_ = self._widgets["view"].model() model_ = model_.sourceModel() @@ -678,7 +673,7 @@ def on_print_code_clicked(self): self._widgets["code"].setText("
".join(pretty)) - def on_application_changed(self, app_request): + def on_application_changed(self): self._widgets["code"].setPlainText("") if not self._widgets["graph"]._pixmapHandle: return diff --git a/allzpark/model.py b/allzpark/model.py index 3ae7547..0758682 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -163,104 +163,100 @@ def parse_icon(root, template): return QtGui.QIcon(fname) -class ResolvedPackagesModel(AbstractTableModel): - ColumnToKey = { - 0: { - QtCore.Qt.DisplayRole: "label", - QtCore.Qt.DecorationRole: "icon", - }, - 1: { - QtCore.Qt.DisplayRole: "version", - }, - 2: { - QtCore.Qt.DisplayRole: "state", - }, - 3: { - QtCore.Qt.DisplayRole: "latest", - }, - 4: { - QtCore.Qt.DisplayRole: "beta", - }, - } +class AbstractPackageItem(dict): - def __init__(self, *args, **kwargs): - super(ResolvedPackagesModel, self).__init__(*args, **kwargs) - self._broken_icon = res.icon("Action_Stop_1_32.png") - self._overrides = {} - self._disabled = {} + def __init__(self, name, package, versions, metadata): + super(AbstractPackageItem, self).__init__({ + "name": name, + "label": metadata["label"], + "icon": parse_icon(package.root, template=metadata["icon"]), + "family": package.name, + "package": package, + "version": str(package.version), + "versions": versions, + "default": str(package.version), + "context": None, + "active": True, + "_hasVersions": len(versions) > 1, + }) - @property - def overrides(self): - return self._overrides - @property - def disabled(self): - return self._disabled +class ApplicationItem(AbstractPackageItem): - def reset(self, packages=None): - packages = packages or dict() + def __init__(self, app_request, app_pkg, versions): + metadata = allzparkconfig.metadata_from_package(app_pkg) + tools = getattr(app_pkg, "tools", None) or [app_pkg.name] - self.beginResetModel() - self.items[:] = [] - - for app_request, app_data in packages.items(): - app = app_data["app"] - name = app_request - versions = app_data["versions"] - self._add_item(name, app_request, app, versions, is_app=True) + super(ApplicationItem, self).__init__(name=app_request, + package=app_pkg, + versions=versions, + metadata=metadata) + self.update({ + "hidden": metadata["hidden"], + "broken": isinstance(app_pkg, BrokenPackage), + "tool": None, # Current tool + "tools": tools, # All available tools + "detached": False, # Open in separate console or not + }) - for pkg, versions in app_data["packages"].items(): - name = pkg.name - self._add_item(name, app_request, pkg, versions) - self.endResetModel() +class PackageItem(AbstractPackageItem): - def _add_item(self, name, app_request, pkg, versions, is_app=False): - root = pkg.root - data = allzparkconfig.metadata_from_package(pkg) - tools = getattr(pkg, "tools", None) or [pkg.name] - version = str(pkg.version) - relocatable = localz.is_relocatable(pkg) if localz else False + def __init__(self, name, package, versions, override, disabled): + metadata = allzparkconfig.metadata_from_package(package) + relocatable = localz.is_relocatable(package) if localz else False state = ( - "(dev)" if is_local(pkg) else - "(localised)" if is_localised(pkg) else + "(dev)" if is_local(package) else + "(localised)" if is_localised(package) else "" ) - item = { - "_isApp": is_app, - "_hasVersions": len(versions) > 1, - - "name": name, - "label": data["label"], - "version": version, - "versions": versions, - "icon": parse_icon(root, template=data["icon"]), - "package": pkg, - "context": None, - "active": True, - - "family": pkg.name, - "request": app_request, - "hidden": data["hidden"], - "broken": isinstance(pkg, BrokenPackage), - - "default": version, - "override": self._overrides.get(pkg.name), - "disabled": self._disabled.get(pkg.name, False), + super(PackageItem, self).__init__(name=name, + package=package, + versions=versions, + metadata=metadata) + self.update({ + "override": override, + "disabled": disabled, "state": state, "relocatable": relocatable, "localizing": False, # in progress + }) + - # Whether or not to open a separate console for this app - "detached": False, - # Current tool - "tool": None, - # All available tools - "tools": tools, +class ApplicationModel(AbstractTableModel): + ColumnToKey = { + 0: { + QtCore.Qt.DisplayRole: "label", + QtCore.Qt.DecorationRole: "icon", + }, + 1: { + QtCore.Qt.DisplayRole: "version", } + } + + Headers = [ + "application", + "version" + ] + + def __init__(self, *args, **kwargs): + super(ApplicationModel, self).__init__(*args, **kwargs) + self._broken_icon = res.icon("Action_Stop_1_32.png") + + def reset(self, applications=None): + applications = applications or dict() - self.items.append(item) + self.beginResetModel() + self.items[:] = [] + + for app_request, data in applications.items(): + app = data["package"] + versions = data["versions"] + item = ApplicationItem(app_request, app, versions) + self.items.append(item) + + self.endResetModel() def data(self, index, role): row = index.row() @@ -292,85 +288,13 @@ def data(self, index, role): if col == 0: return self._broken_icon - if data["override"]: - if role == QtCore.Qt.DisplayRole and col == 1: - return data["override"] - - if role == QtCore.Qt.FontRole: - font = QtGui.QFont() - font.setBold(True) - return font - - if role == QtCore.Qt.ForegroundRole: - return QtGui.QColor("darkorange") - - if data["disabled"] or data["localizing"]: - if role == QtCore.Qt.FontRole: - font = QtGui.QFont() - font.setBold(True) - font.setStrikeOut(True) - return font - - if role == QtCore.Qt.ForegroundRole: - return QtGui.QColor("darkorange") - if data["_hasVersions"] and col == 1: if role == QtCore.Qt.FontRole: font = QtGui.QFont() font.setBold(True) return font - try: - value = data[role] - - if isinstance(value, list): - # Prevent edits - value = value[:] - - return value - - except KeyError: - try: - key = self.ColumnToKey[col][role] - except KeyError: - return None - - if key == "beta": - version = data["override"] or data["version"] - return "x" if re.findall(r".beta$", version) else "" - - if key == "latest": - version = data["override"] or data["version"] - latest = data["versions"][-1] - return "x" if version == latest else "" - - return data[key] - - def setData(self, index, value, role): - if role == "override": - default = self.data(index, "default") - package = self.data(index, "package").name - - if value and value != default: - log.info("Storing permanent override %s-%s" % (package, value)) - self._overrides[package] = value - else: - log.info("Resetting to default") - self._overrides.pop(package, None) - value = None - - if role == "disabled": - package = self.data(index, "package").name - value = bool(value) - - if value: - log.info("Disabling %s" % package) - else: - log.info("Enabling %s" % package) - - self._disabled[package] = value - - return super(ResolvedPackagesModel, self).setData(index, value, role) + return super(ApplicationModel, self).data(index, role) def flags(self, index): if index.column() == 1: @@ -380,7 +304,7 @@ def flags(self, index): QtCore.Qt.ItemIsEditable ) - return super(ResolvedPackagesModel, self).flags(index) + return super(ApplicationModel, self).flags(index) class BrokenContext(object): @@ -451,6 +375,150 @@ def is_localised(pkg): return False +class PackagesModel(AbstractTableModel): + ColumnToKey = { + 0: { + QtCore.Qt.DisplayRole: "label", + QtCore.Qt.DecorationRole: "icon", + }, + 1: { + QtCore.Qt.DisplayRole: "version", + }, + 2: { + QtCore.Qt.DisplayRole: "state", + }, + 3: { + QtCore.Qt.DisplayRole: "latest", + }, + 4: { + QtCore.Qt.DisplayRole: "beta", + }, + } + + Headers = [ + "package", + "version", + "state", + "latest", + "beta", + ] + + def __init__(self, parent=None): + super(PackagesModel, self).__init__(parent) + self._overrides = {} + self._disabled = {} + + def reset(self, packages=None): + packages = packages or dict() + + self.beginResetModel() + self.items[:] = [] + + for name, data in packages.items(): + pkg = data["package"] + versions = data["versions"] + override = self._overrides.get(pkg.name) + disabled = self._disabled.get(pkg.name, False) + + item = PackageItem(name, pkg, versions, override, disabled) + self.items.append(item) + + self.endResetModel() + + def data(self, index, role): + row = index.row() + col = index.column() + + try: + data = self.items[row] + except IndexError: + return None + + if data["override"]: + if role == QtCore.Qt.DisplayRole and col == 1: + return data["override"] + + if role == QtCore.Qt.FontRole: + font = QtGui.QFont() + font.setBold(True) + return font + + if role == QtCore.Qt.ForegroundRole: + return QtGui.QColor("darkorange") + + if data["disabled"] or data["localizing"]: + if role == QtCore.Qt.FontRole: + font = QtGui.QFont() + font.setBold(True) + font.setStrikeOut(True) + return font + + if role == QtCore.Qt.ForegroundRole: + return QtGui.QColor("darkorange") + + if data["_hasVersions"] and col == 1: + if role == QtCore.Qt.FontRole: + font = QtGui.QFont() + font.setBold(True) + return font + + try: + return data[role] + + except KeyError: + try: + key = self.ColumnToKey[col][role] + except KeyError: + return None + + if key == "beta": + version = data["override"] or data["version"] + return "x" if re.findall(r".beta$", version) else "" + + if key == "latest": + version = data["override"] or data["version"] + latest = data["versions"][-1] + return "x" if version == latest else "" + + return data[key] + + def setData(self, index, value, role): + if role == "override": + default = self.data(index, "default") + package = self.data(index, "package").name + + if value and value != default: + log.info("Storing permanent override %s-%s" % (package, value)) + self._overrides[package] = value + else: + log.info("Resetting to default") + self._overrides.pop(package, None) + value = None + + if role == "disabled": + package = self.data(index, "package").name + value = bool(value) + + if value: + log.info("Disabling %s" % package) + else: + log.info("Enabling %s" % package) + + self._disabled[package] = value + + return super(PackagesModel, self).setData(index, value, role) + + def flags(self, index): + if index.column() == 1: + return ( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable | + QtCore.Qt.ItemIsEditable + ) + + return super(PackagesModel, self).flags(index) + + class CommandsModel(AbstractTableModel): ColumnToKey = { 0: { @@ -637,42 +705,6 @@ def rowCount(self, parent=QtCore.QModelIndex()): return super(ProxyModel, self).rowCount(parent) -class ApplicationProxyModel(ProxyModel): - - Headers = [ - "application", - "version" - ] - - def columnCount(self, parent): - return len(self.Headers) - - def headerData(self, section, orientation, role): - if orientation == QtCore.Qt.Vertical: - return - - if role == QtCore.Qt.DisplayRole: - return self.Headers[section] - - -class PackagesProxyModel(ProxyModel): - - Headers = [ - "package", - "version", - "state", - "latest", - "beta", - ] - - def headerData(self, section, orientation, role): - if orientation == QtCore.Qt.Vertical: - return - - if role == QtCore.Qt.DisplayRole: - return self.Headers[section] - - class TreeItem(dict): def __init__(self, data=None): diff --git a/allzpark/view.py b/allzpark/view.py index 9a64146..cea0e03 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -59,13 +59,6 @@ def on_state_appok(self): def is_selected_app_ok(self): return self._selected_app_ok - def setModel(self, model_): - proxy_model = model.ApplicationProxyModel(model_) - proxy_model.setup(include=[ - ("_isApp", True) - ]) - super(Applications, self).setModel(proxy_model) - class Window(QtWidgets.QMainWindow): title = "Allzpark %s" % version @@ -299,14 +292,15 @@ def on_visible(widget, toggle, state): docks["profiles"].set_model(ctrl.models["profiles"], ctrl.models["profileVersions"]) - docks["packages"].set_model(ctrl.models["resolved"]) + docks["packages"].set_model(ctrl.models["packages"]) docks["context"].set_model(ctrl.models["context"]) docks["environment"].set_model(ctrl.models["environment"], ctrl.models["parentenv"], ctrl.models["diagnose"]) docks["commands"].set_model(ctrl.models["commands"]) - widgets["apps"].setModel(ctrl.models["resolved"]) + proxy_model = model.ProxyModel(ctrl.models["apps"]) + widgets["apps"].setModel(proxy_model) widgets["errorMessage"].setAlignment(QtCore.Qt.AlignHCenter) @@ -329,7 +323,7 @@ def on_visible(widget, toggle, state): selection_model = widgets["apps"].selectionModel() selection_model.selectionChanged.connect(self.on_app_selection_changed) - ctrl.models["resolved"].modelReset.connect(self.on_apps_reset) + ctrl.models["apps"].modelReset.connect(self.on_apps_reset) ctrl.models["profiles"].modelReset.connect( self.on_profilename_reset) ctrl.models["profileVersions"].modelReset.connect( @@ -342,7 +336,6 @@ def on_visible(widget, toggle, state): ctrl.repository_changed.connect(self.on_repository_changed) ctrl.command_changed.connect(self.on_command_changed) ctrl.application_changed.connect(self.on_app_changed) - ctrl.application_changed.connect(docks["packages"].on_app_changed) self._pages = pages self._widgets = widgets @@ -739,7 +732,7 @@ def on_apps_reset(self): app = self._ctrl.state.retrieve("startupApplication") row = 0 - model = self._ctrl.models["resolved"] + model = self._ctrl.models["apps"] if app: for row_ in range(model.rowCount()): @@ -782,7 +775,7 @@ def on_app_selection_changed(self, selected, deselected): app_request = model.data(index, "name") self._ctrl.select_application(app_request) - def on_app_changed(self, app_request): + def on_app_changed(self): selection_model = self._widgets["apps"].selectionModel() index = selection_model.selectedIndexes()[0] self._docks["app"].refresh(index) From 81d3ff74380a38b170172a9eb47fe98d4186d0da Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 13 Nov 2020 23:54:44 +0800 Subject: [PATCH 12/28] fix bad revert --- allzpark/control.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 1e2a31b..4d3e84b 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -751,8 +751,8 @@ def do(): "This is a bug" ) - overrides = self._models["packages"].overrides - disabled = self._models["packages"].disabled + overrides = self._models["packages"]._overrides + disabled = self._models["packages"]._disabled environ = self.parent_environ() self.debug( @@ -1016,13 +1016,6 @@ def select_application(self, app_request): self._state.to_ready() - if context.success: - self._state.to_appok() - else: - self._state.to_appfailed() - - self._state.to_ready() - def select_tool(self, tool_name): self._state["tool"] = tool_name self.update_command() From 7b810dd2c73ebd4664292917f478d9d7f306c34d Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 02:54:54 +0800 Subject: [PATCH 13/28] fix package versions picking condition --- allzpark/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 4d3e84b..0449541 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -362,10 +362,10 @@ def resolved_packages(self, app_request): packages = odict() # keep resolved order for pkg in resolved or []: # profile version should not be changed in Packages view - _all_vers = pkg.name != profile_name + _all_vers = all_vers and pkg.name != profile_name versions = [ str(p.version) - for p in (self.find(pkg.name) if all_vers else [pkg]) + for p in (self.find(pkg.name) if _all_vers else [pkg]) ] packages[pkg.name] = { "package": pkg, From b0f3224cda19303877256532f28d40843ea63817 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 02:55:05 +0800 Subject: [PATCH 14/28] add tests --- tests/test_apps.py | 30 +++------------ tests/test_docks.py | 69 +++++++++++++++++++++++++++++---- tests/test_launch.py | 5 +-- tests/test_profiles.py | 5 +-- tests/util.py | 87 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 151 insertions(+), 45 deletions(-) diff --git a/tests/test_apps.py b/tests/test_apps.py index 6fba619..eb2730d 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -31,10 +31,7 @@ def test_select_app(self): } }, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) with self.wait_signal(self.ctrl.state_changed, "ready"): self.ctrl.select_profile("foo") @@ -82,10 +79,7 @@ def test_app_environ(self): } }, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) with self.wait_signal(self.ctrl.state_changed, "ready"): self.ctrl.select_profile("foo") @@ -134,10 +128,7 @@ def test_app_failed_independently_1(self): "app_A": {"1": {"name": "app_A", "version": "1"}}, "app_B": {"1": {"name": "app_B", "version": "1"}}, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) context_a = self.ctrl.state["rezContexts"]["app_A==1"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] @@ -160,10 +151,7 @@ def test_app_failed_independently_2(self): }, "app_B": {"1": {"name": "app_B", "version": "1"}}, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) context_a = self.ctrl.state["rezContexts"]["app_A==None"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] @@ -193,10 +181,7 @@ def test_app_failed_independently_3(self): }, "app_B": {"1": {"name": "app_B", "version": "1"}}, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) context_a = self.ctrl.state["rezContexts"]["app_A==1"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] @@ -220,10 +205,7 @@ def test_app_failed_independently_4(self): "app_A": {"1": {"name": "app_A", "version": "1"}}, "app_B": {"1": {"name": "app_B", "version": "1"}}, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) context_a = self.ctrl.state["rezContexts"]["app_A==2"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] diff --git a/tests/test_docks.py b/tests/test_docks.py index e3d0620..a3d26c4 100644 --- a/tests/test_docks.py +++ b/tests/test_docks.py @@ -6,6 +6,8 @@ class TestDocks(util.TestBase): def test_feature_blocked_on_failed_app(self): """Test feature blocked if application is broken""" + self.set_preference("showAdvancedControls", True) + util.memory_repository({ "foo": { "1.0.0": { @@ -19,10 +21,7 @@ def test_feature_blocked_on_failed_app(self): }, "app_B": {"1": {"name": "app_B", "version": "1"}}, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) context_a = self.ctrl.state["rezContexts"]["app_A==None"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] @@ -30,11 +29,8 @@ def test_feature_blocked_on_failed_app(self): self.assertFalse(context_a.success) self.assertTrue(context_b.success) - self.show_advance_controls() - for app, state in {"app_A==None": False, "app_B==1": True}.items(): - self.ctrl.select_application(app) - self.wait(100) + self.select_application(app) dock = self.show_dock("environment", on_page="diagnose") self.assertEqual(dock._widgets["compute"].isEnabled(), state) @@ -44,3 +40,60 @@ def test_feature_blocked_on_failed_app(self): dock = self.show_dock("app") self.assertEqual(dock._widgets["launchBtn"].isEnabled(), state) + + def test_version_editable_on_show_all_versions(self): + """Test version is editable when show all version enabled""" + self._test_version_editable(show_all_version=True) + + def test_version_editable_on_not_show_all_versions(self): + """Test version is not editable when show all version disabled""" + self._test_version_editable(show_all_version=False) + + def _test_version_editable(self, show_all_version): + self.set_preference("showAdvancedControls", True) + self.set_preference("showAllVersions", show_all_version) + + util.memory_repository({ + "foo": { + "1": {"name": "foo", "version": "1", + "requires": ["~app_A", "~app_B"]}, + "2": {"name": "foo", "version": "2", + "requires": ["~app_A", "~app_B"]}, + }, + "app_A": {"1": {"name": "app_A", "version": "1"}}, + "app_B": {"1": {"name": "app_B", "version": "1", + "requires": ["bar"]}}, + "bar": {"1": {"name": "bar", "version": "1"}, + "2": {"name": "bar", "version": "2"}} + }) + self.ctrl_reset(["foo"]) + self.select_application("app_B==1") + + dock = self.show_dock("packages") + view = dock._widgets["view"] + proxy = view.model() + model = proxy.sourceModel() + + for pkg, state in {"foo": False, # profile can't change version here + "bar": show_all_version, + "app_B": False}.items(): + index = model.findIndex(pkg) + index = proxy.mapFromSource(index) + + rect = view.visualRect(index) + position = rect.center() + with util.patch_cursor_pos(view.mapToGlobal(position)): + dock.on_right_click(position) + menu = self.get_menu(dock) + edit_action = next((a for a in menu.actions() + if a.text() == "Edit"), None) + if edit_action is None: + self.fail("No version edit action.") + + self.assertEqual( + edit_action.isEnabled(), state, + "Package '%s' version edit state is incorrect." % pkg + ) + + self.wait(200) + menu.close() diff --git a/tests/test_launch.py b/tests/test_launch.py index 7c3a1d7..ed663d0 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -22,10 +22,7 @@ def test_launch_subprocess(self): } }, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) with self.wait_signal(self.ctrl.state_changed, "ready"): self.ctrl.select_profile("foo") diff --git a/tests/test_profiles.py b/tests/test_profiles.py index a160f61..b81252d 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -93,10 +93,7 @@ def test_profile_list_apps(self): } }, }) - with self.wait_signal(self.ctrl.resetted): - self.ctrl.reset(["foo"]) - self.wait(timeout=200) - self.assertEqual(self.ctrl.state.state, "ready") + self.ctrl_reset(["foo"]) with self.wait_signal(self.ctrl.state_changed, "ready"): self.ctrl.select_profile("foo") diff --git a/tests/util.py b/tests/util.py index aa6a255..2e8967a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -9,10 +9,16 @@ def memory_repository(packages): + from rezplugins.package_repository import memory from allzpark import _rezapi as rez + class MemoryVariantRes(memory.MemoryVariantResource): + def _root(self): # implement `root` to work with localz + return MEMORY_LOCATION + manager = rez.package_repository_manager repository = manager.get_repository(MEMORY_LOCATION) + repository.pool.resource_classes[MemoryVariantRes.key] = MemoryVariantRes repository.data = packages @@ -27,6 +33,9 @@ def setUp(self): app, ctrl = cli.initialize(clean=True, verbose=3) window = cli.launch(ctrl) + size = window.size() + window.resize(size.width() + 80, size.height() + 80) + self.app = app self.ctrl = ctrl self.window = window @@ -38,17 +47,47 @@ def tearDown(self): self.window.close() time.sleep(0.1) - def show_advance_controls(self): + def set_preference(self, name, value): + """Setup preference + + This should be called before ctrl reset. + + (NOTE) Some preference change may calling ctrl.reset, so ctrl.reset + will be monkey-patched to do nothing, this is to prevent error + raised from resting without profile. + If not doing this, and setup preference after reset with test + profiles, calling reset again on preference changed may leads + to some weird profile model reset error. (item.internalPointer + returning random object and AttributeError raised) + + Args: + name: preference name + value: preference value + + Returns: + None + + """ preferences = self.window._docks["preferences"] - arg = next(opt for opt in preferences.options - if opt["name"] == "showAdvancedControls") - arg.write(True) + arg = next((opt for opt in preferences.options + if opt["name"] == name), None) + if not arg: + self.fail("Preference doesn't have this setting: %s" % name) + + origin_reset = getattr(self.ctrl, "reset") + setattr(self.ctrl, "reset", lambda: None) + try: + arg.write(value) + except Exception as e: + self.fail("Preference '%s' set failed: %s" % (name, str(e))) + finally: + setattr(self.ctrl, "reset", origin_reset) def show_dock(self, name, on_page=None): dock = self.window._docks[name] dock.toggle.setChecked(True) dock.toggle.clicked.emit() - self.wait(timeout=200) + self.wait(timeout=50) if on_page is not None: tabs = dock._panels["central"] @@ -58,6 +97,23 @@ def show_dock(self, name, on_page=None): return dock + def ctrl_reset(self, profiles): + with self.wait_signal(self.ctrl.resetted): + self.ctrl.reset(profiles) + self.wait(timeout=200) + self.assertEqual(self.ctrl.state.state, "ready") + + def select_application(self, app_request): + apps = self.window._widgets["apps"] + proxy = apps.model() + model = proxy.sourceModel() + index = model.findIndex(app_request) + index = proxy.mapFromSource(index) + + sel_model = apps.selectionModel() + sel_model.select(index, sel_model.ClearAndSelect | sel_model.Rows) + self.wait(50) + def wait(self, timeout=1000): from allzpark.vendor.Qt import QtCore @@ -106,3 +162,24 @@ def on_timeout(): if not state["received"]: timer.start(timeout) loop.exec_() + + def get_menu(self, widget): + from allzpark.vendor.Qt import QtWidgets + menus = widget.findChildren(QtWidgets.QMenu, "") + menu = next((m for m in menus if m.isVisible()), None) + if menu: + return menu + else: + self.fail("This widget doesn't have menu.") + + +@contextlib.contextmanager +def patch_cursor_pos(point): + from allzpark.vendor.Qt import QtGui + + origin_pos = getattr(QtGui.QCursor, "pos") + setattr(QtGui.QCursor, "pos", lambda: point) + try: + yield + finally: + setattr(QtGui.QCursor, "pos", origin_pos) From feed9c35bbaf472a4be117bb565520eb164f1320 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 17:38:49 +0800 Subject: [PATCH 15/28] ensure widgets can be garbage collected To be more test friendly. Widgets (qargparse widgets in this case) were defined as class attributes (not in __init__), so it won't be garbage collected after main window closed, and signals are still connected. And somehow they still able to trigger the model which is created in next test session to do model reset. So how many tests has ran, how many reset signal will be emitted, to the same model, hence the test can easily got segfault (especially Linux). --- allzpark/dock.py | 175 ++++++++++++++++++++++---------------------- tests/test_docks.py | 12 +-- tests/util.py | 28 +------ 3 files changed, 97 insertions(+), 118 deletions(-) diff --git a/allzpark/dock.py b/allzpark/dock.py index d06e23d..72c8a15 100644 --- a/allzpark/dock.py +++ b/allzpark/dock.py @@ -1060,97 +1060,100 @@ class Preferences(AbstractDockWidget): icon = "Action_GoHome_32" - options = [ - qargparse.Info("startupProfile", help=( - "Load this profile on startup" - )), - qargparse.Info("startupApplication", help=( - "Load this application on startup" - )), - - qargparse.Separator("Appearance"), - - qargparse.Enum("theme", items=res.theme_names(), help=( - "GUI skin. May need to restart Allzpark after changed." - )), - - qargparse.Button("resetLayout", help=( - "Reset stored layout to their defaults" - )), - - qargparse.Separator("Settings"), - - qargparse.Boolean("smallIcons", enabled=False, help=( - "Draw small icons" - )), - qargparse.Boolean("allowMultipleDocks", help=( - "Allow more than one dock to exist at a time" - )), - qargparse.Boolean("showAdvancedControls", help=( - "Show developer-centric controls" - )), - qargparse.Boolean("showAllApps", help=( - "List everything from allzparkconfig:applications\n" - "not just the ones specified for a given profile." - )), - qargparse.Boolean("showHiddenApps", help=( - "Show apps with metadata['hidden'] = True" - )), - qargparse.Boolean("showAllVersions", help=( - "Show all package versions.\n" - "Profile requested application version range will still be \n" - "respected, but all versions of each dependency package will \n" - "be shown." - )), - qargparse.Boolean("patchWithFilter", help=( - "Use the current exclusion filter when patching.\n" - "This enables patching of packages outside of a filter, \n" - "such as *.beta packages, with every other package still \n" - "qualifying for that filter." - )), - qargparse.Integer("clearCacheTimeout", min=1, default=10, help=( - "Clear package repository cache at this interval, in seconds. \n\n" - - "Default 10. (Requires restart)\n\n" - - "Normally, filesystem calls like `os.listdir` are cached \n" - "so as to avoid unnecessary calls. However, whenever a new \n" - "version of a package is released, it will remain invisible \n" - "until this cache is cleared. \n\n" - - "Clearing ths cache should have a very small impact on \n" - "performance and is safe to do frequently. It has no effect \n" - "on memcached which has a much greater impact on performanc." - )), - - qargparse.String( - "exclusionFilter", - default=allzparkconfig.exclude_filter, - help="Exclude versions that match this expression"), - - qargparse.Separator("System"), - - # Provided by controller - qargparse.Info("pythonExe"), - qargparse.Info("pythonVersion"), - qargparse.Info("qtVersion"), - qargparse.Info("qtBinding"), - qargparse.Info("qtBindingVersion"), - qargparse.Info("rezLocation"), - qargparse.Info("rezVersion"), - qargparse.Info("rezConfigFile"), - qargparse.Info("memcachedURI"), - qargparse.InfoList("rezPackagesPath"), - qargparse.InfoList("rezLocalPath"), - qargparse.InfoList("rezReleasePath"), - qargparse.Info("settingsPath"), - ] - def __init__(self, window, ctrl, parent=None): super(Preferences, self).__init__("Preferences", parent) self.setAttribute(QtCore.Qt.WA_StyledBackground) self.setObjectName("Preferences") + self.options = [ + qargparse.Info("startupProfile", help=( + "Load this profile on startup" + )), + qargparse.Info("startupApplication", help=( + "Load this application on startup" + )), + + qargparse.Separator("Appearance"), + + qargparse.Enum("theme", items=res.theme_names(), help=( + "GUI skin. May need to restart Allzpark after changed." + )), + + qargparse.Button("resetLayout", help=( + "Reset stored layout to their defaults" + )), + + qargparse.Separator("Settings"), + + qargparse.Boolean("smallIcons", enabled=False, help=( + "Draw small icons" + )), + qargparse.Boolean("allowMultipleDocks", help=( + "Allow more than one dock to exist at a time" + )), + qargparse.Boolean("showAdvancedControls", help=( + "Show developer-centric controls" + )), + qargparse.Boolean("showAllApps", help=( + "List everything from allzparkconfig:applications\n" + "not just the ones specified for a given profile." + )), + qargparse.Boolean("showHiddenApps", help=( + "Show apps with metadata['hidden'] = True" + )), + qargparse.Boolean("showAllVersions", help=( + "Show all package versions.\n" + "Profile requested application version range will \n" + "still be respected, but all versions of each \n" + "dependency package will be shown." + )), + qargparse.Boolean("patchWithFilter", help=( + "Use the current exclusion filter when patching.\n" + "This enables patching of packages outside of a \n" + "filter, such as *.beta packages, with every other \n" + "package still qualifying for that filter." + )), + qargparse.Integer("clearCacheTimeout", min=1, default=10, help=( + "Clear package repository cache at this interval, in \n" + "seconds.\n\n" + + "Default 10. (Requires restart)\n\n" + + "Normally, filesystem calls like `os.listdir` are \n" + "cached so as to avoid unnecessary calls. However, \n" + "whenever a new version of a package is released, \n" + "it will remain invisible until this cache is \n" + "cleared. \n\n" + + "Clearing ths cache should have a very small impact \n" + "on performance and is safe to do frequently. It has \n" + "no effect on memcached which has a much greater \n" + "impact on performance." + )), + + qargparse.String( + "exclusionFilter", + default=allzparkconfig.exclude_filter, + help="Exclude versions that match this expression"), + + qargparse.Separator("System"), + + # Provided by controller + qargparse.Info("pythonExe"), + qargparse.Info("pythonVersion"), + qargparse.Info("qtVersion"), + qargparse.Info("qtBinding"), + qargparse.Info("qtBindingVersion"), + qargparse.Info("rezLocation"), + qargparse.Info("rezVersion"), + qargparse.Info("rezConfigFile"), + qargparse.Info("memcachedURI"), + qargparse.InfoList("rezPackagesPath"), + qargparse.InfoList("rezLocalPath"), + qargparse.InfoList("rezReleasePath"), + qargparse.Info("settingsPath"), + ] + protected = allzparkconfig.protected_preferences() for name, value in protected.items(): arg = next((a for a in self.options if a["name"] == name), None) diff --git a/tests/test_docks.py b/tests/test_docks.py index a3d26c4..5b2cd0c 100644 --- a/tests/test_docks.py +++ b/tests/test_docks.py @@ -6,8 +6,6 @@ class TestDocks(util.TestBase): def test_feature_blocked_on_failed_app(self): """Test feature blocked if application is broken""" - self.set_preference("showAdvancedControls", True) - util.memory_repository({ "foo": { "1.0.0": { @@ -23,6 +21,8 @@ def test_feature_blocked_on_failed_app(self): }) self.ctrl_reset(["foo"]) + self.set_preference("showAdvancedControls", True) + context_a = self.ctrl.state["rezContexts"]["app_A==None"] context_b = self.ctrl.state["rezContexts"]["app_B==1"] @@ -50,9 +50,6 @@ def test_version_editable_on_not_show_all_versions(self): self._test_version_editable(show_all_version=False) def _test_version_editable(self, show_all_version): - self.set_preference("showAdvancedControls", True) - self.set_preference("showAllVersions", show_all_version) - util.memory_repository({ "foo": { "1": {"name": "foo", "version": "1", @@ -67,6 +64,11 @@ def _test_version_editable(self, show_all_version): "2": {"name": "bar", "version": "2"}} }) self.ctrl_reset(["foo"]) + + self.set_preference("showAdvancedControls", True) + self.set_preference("showAllVersions", show_all_version) + self.wait(200) # wait for reset + self.select_application("app_B==1") dock = self.show_dock("packages") diff --git a/tests/util.py b/tests/util.py index 2e8967a..ac35378 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,6 +1,5 @@ import os -import time import unittest import contextlib @@ -43,45 +42,20 @@ def setUp(self): self.wait(timeout=50) def tearDown(self): - self.wait(timeout=500) + self.wait(timeout=200) self.window.close() - time.sleep(0.1) def set_preference(self, name, value): - """Setup preference - - This should be called before ctrl reset. - - (NOTE) Some preference change may calling ctrl.reset, so ctrl.reset - will be monkey-patched to do nothing, this is to prevent error - raised from resting without profile. - If not doing this, and setup preference after reset with test - profiles, calling reset again on preference changed may leads - to some weird profile model reset error. (item.internalPointer - returning random object and AttributeError raised) - - Args: - name: preference name - value: preference value - - Returns: - None - - """ preferences = self.window._docks["preferences"] arg = next((opt for opt in preferences.options if opt["name"] == name), None) if not arg: self.fail("Preference doesn't have this setting: %s" % name) - origin_reset = getattr(self.ctrl, "reset") - setattr(self.ctrl, "reset", lambda: None) try: arg.write(value) except Exception as e: self.fail("Preference '%s' set failed: %s" % (name, str(e))) - finally: - setattr(self.ctrl, "reset", origin_reset) def show_dock(self, name, on_page=None): dock = self.window._docks[name] From 912918e901c4d9c53f714aa95928bc52037561b4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 17:47:14 +0800 Subject: [PATCH 16/28] add back time wait --- tests/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index ac35378..91bb039 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,5 +1,6 @@ import os +import time import unittest import contextlib @@ -42,8 +43,9 @@ def setUp(self): self.wait(timeout=50) def tearDown(self): - self.wait(timeout=200) + self.wait(timeout=500) self.window.close() + time.sleep(0.1) def set_preference(self, name, value): preferences = self.window._docks["preferences"] From b2af2245511e88fd1b1f834dabaa1244852db567 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 22:34:30 +0800 Subject: [PATCH 17/28] no need to reset on showAllVersions --- allzpark/view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allzpark/view.py b/allzpark/view.py index cea0e03..2a7e7c3 100644 --- a/allzpark/view.py +++ b/allzpark/view.py @@ -500,10 +500,12 @@ def on_setting_changed(self, argument): if key in ("showAllApps", "showHiddenApps", - "showAllVersions", "patchWithFilter"): self._ctrl.reset() + if key == "showAllVersions": + self._ctrl.select_application(self._ctrl.state["appRequest"]) + if key == "exclusionFilter": allzparkconfig.exclude_filter = value self._ctrl.reset() From cff65cf5f9112d8a45e013edb05e384eed3590eb Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 22:54:36 +0800 Subject: [PATCH 18/28] store startup app on version change --- allzpark/delegates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allzpark/delegates.py b/allzpark/delegates.py index b72502a..df6c0ac 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -53,4 +53,6 @@ def setModelData(self, editor, model, index): if not version or version == default: return - self._ctrl.patch("%s==%s" % (package, version)) + app_request = "%s==%s" % (package, version) + self._ctrl.state.store("startupApplication", app_request) + self._ctrl.patch(app_request) From 2437eab312345bad75302affbc0617eba5a60781 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 22:54:54 +0800 Subject: [PATCH 19/28] add app version changing test --- allzpark/model.py | 7 +++---- tests/test_apps.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/allzpark/model.py b/allzpark/model.py index 0758682..7344683 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -79,10 +79,9 @@ def reset(self, items=None): def find(self, name): return next(i for i in self.items if i["name"] == name) - def findIndex(self, name): - return self.createIndex( - self.items.index(self.find(name)), 0, QtCore.QModelIndex() - ) + def findIndex(self, name, column=0): + row = self.items.index(self.find(name)) + return self.createIndex(row, column, QtCore.QModelIndex()) def rowCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): diff --git a/tests/test_apps.py b/tests/test_apps.py index eb2730d..2588242 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -1,4 +1,5 @@ +from unittest import mock from tests import util @@ -212,3 +213,52 @@ def test_app_failed_independently_4(self): self.assertFalse(context_a.success) self.assertTrue(context_b.success) + + def test_app_changing_version(self): + """Test application version can be changed in view""" + util.memory_repository({ + "foo": { + "1": {"name": "foo", "version": "1", + "requires": ["~app_A", "~app_B"]} + }, + "app_A": {"1": {"name": "app_A", "version": "1"}}, + "app_B": {"1": {"name": "app_B", "version": "1"}, + "2": {"name": "app_B", "version": "2"},} + }) + self.ctrl_reset(["foo"]) + self.show_dock("app") + + apps = self.window._widgets["apps"] + + def get_version_editor(app_request): + self.select_application(app_request) + proxy = apps.model() + model = proxy.sourceModel() + index = model.findIndex(app_request, column=1) + index = proxy.mapFromSource(index) + apps.edit(index) + + return apps.indexWidget(index), apps.itemDelegate(index) + + editor, delegate = get_version_editor("app_A==1") + self.assertIsNone( + editor, "No version editing if App has only one version.") + + editor, delegate = get_version_editor("app_B==2") + self.assertIsNotNone( + editor, "Version should be editable if App has versions.") + + # for visual + editor.showPopup() + self.wait(100) + view = editor.view() + index = view.model().index(0, 0) + sel_model = view.selectionModel() + sel_model.select(index, sel_model.ClearAndSelect) + self.wait(150) + # change version + editor.setCurrentIndex(0) + delegate.commitData.emit(editor) + self.wait(200) # wait patch + + self.assertEqual("app_B==1", self.ctrl.state["appRequest"]) From d6f1fa13d2e822278f2d02ad3e7046a92c52979f Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 22:55:53 +0800 Subject: [PATCH 20/28] delete qobject on test teardown --- tests/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/util.py b/tests/util.py index 91bb039..538b5a5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -45,6 +45,8 @@ def setUp(self): def tearDown(self): self.wait(timeout=500) self.window.close() + self.ctrl.deleteLater() + self.window.deleteLater() time.sleep(0.1) def set_preference(self, name, value): From 84e7d58bad89caa3dbd34c41b2a96710f36843f4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 23:33:28 +0800 Subject: [PATCH 21/28] devop ubuntu pip install pyside2 pyside2 from apt-get is at 5.12.8, which seems not able to get delegate widget from index. pip install latest pyside2 solved the problem (5.15.1). --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 624d3d6..80b6f24 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -85,6 +85,7 @@ jobs: python3-pyside2.qtgui \ python3-pyside2.qtwidgets \ python3-pyside2.qtsvg + sudo pip install pyside2 sudo python -c "from PySide2 import QtCore;print(QtCore.__version__)" condition: startsWith(variables['python.version'], '3.') displayName: "Install PySide2" From eb39f5dfb10b954593dc6582a0e5a1db3408e8d4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 14 Nov 2020 23:43:45 +0800 Subject: [PATCH 22/28] avoid saving non-app package as startup --- allzpark/delegates.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/allzpark/delegates.py b/allzpark/delegates.py index df6c0ac..b76b7f1 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -53,6 +53,11 @@ def setModelData(self, editor, model, index): if not version or version == default: return - app_request = "%s==%s" % (package, version) - self._ctrl.state.store("startupApplication", app_request) - self._ctrl.patch(app_request) + patch_request = "%s==%s" % (package, version) + + for app_pkg in self._ctrl.state["rezApps"].values(): + if app_pkg.name == package: + self._ctrl.state.store("startupApplication", patch_request) + break + + self._ctrl.patch(patch_request) From 1b085eef0ff2667f435c2c9628b2ab36ca676cee Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 02:05:34 +0800 Subject: [PATCH 23/28] polish a bit --- allzpark/control.py | 15 ++++++++++++--- allzpark/model.py | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 0449541..a9cdd18 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -356,16 +356,25 @@ def environ(self, app_request): return environ def resolved_packages(self, app_request): + """Return context resolved packages and versions + + If preference 'showAllVersions' is enabled, all packages' versions + will be collected, except profile. Profile version should not be + changed from Packages view, should be changed from Profile view. + + """ all_vers = self._state.retrieve("showAllVersions", False) profile_name = self._state["profileName"] resolved = self._state["rezContexts"][app_request].resolved_packages + packages = odict() # keep resolved order for pkg in resolved or []: - # profile version should not be changed in Packages view - _all_vers = all_vers and pkg.name != profile_name + is_profile = pkg.name == profile_name + all_vers_ = all_vers and not is_profile + versions = [ str(p.version) - for p in (self.find(pkg.name) if _all_vers else [pkg]) + for p in (self.find(pkg.name) if all_vers_ else [pkg]) ] packages[pkg.name] = { "package": pkg, diff --git a/allzpark/model.py b/allzpark/model.py index 7344683..62ae6fa 100644 --- a/allzpark/model.py +++ b/allzpark/model.py @@ -182,7 +182,9 @@ def __init__(self, name, package, versions, metadata): class ApplicationItem(AbstractPackageItem): - def __init__(self, app_request, app_pkg, versions): + def __init__(self, app_request, data): + app_pkg = data["package"] + versions = data["versions"] metadata = allzparkconfig.metadata_from_package(app_pkg) tools = getattr(app_pkg, "tools", None) or [app_pkg.name] @@ -201,7 +203,9 @@ def __init__(self, app_request, app_pkg, versions): class PackageItem(AbstractPackageItem): - def __init__(self, name, package, versions, override, disabled): + def __init__(self, name, data): + package = data["package"] + versions = data["versions"] metadata = allzparkconfig.metadata_from_package(package) relocatable = localz.is_relocatable(package) if localz else False state = ( @@ -215,8 +219,8 @@ def __init__(self, name, package, versions, override, disabled): versions=versions, metadata=metadata) self.update({ - "override": override, - "disabled": disabled, + "override": data["override"], + "disabled": data["disabled"], "state": state, "relocatable": relocatable, "localizing": False, # in progress @@ -250,9 +254,7 @@ def reset(self, applications=None): self.items[:] = [] for app_request, data in applications.items(): - app = data["package"] - versions = data["versions"] - item = ApplicationItem(app_request, app, versions) + item = ApplicationItem(app_request, data) self.items.append(item) self.endResetModel() @@ -414,12 +416,10 @@ def reset(self, packages=None): self.items[:] = [] for name, data in packages.items(): - pkg = data["package"] - versions = data["versions"] - override = self._overrides.get(pkg.name) - disabled = self._disabled.get(pkg.name, False) + data["override"] = self._overrides.get(name) + data["disabled"] = self._disabled.get(name, False) - item = PackageItem(name, pkg, versions, override, disabled) + item = PackageItem(name, data) self.items.append(item) self.endResetModel() From 36416064a53ae9215af9571445dc19c2257587e9 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 20:41:06 +0800 Subject: [PATCH 24/28] add back app version range in Packages --- allzpark/control.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index a9cdd18..d8e0d5b 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -364,18 +364,26 @@ def resolved_packages(self, app_request): """ all_vers = self._state.retrieve("showAllVersions", False) + app_vers = self._models["apps"].find(app_request)["versions"] + app_names = {app.name for app in self._state["rezApps"].values()} profile_name = self._state["profileName"] resolved = self._state["rezContexts"][app_request].resolved_packages packages = odict() # keep resolved order for pkg in resolved or []: is_profile = pkg.name == profile_name - all_vers_ = all_vers and not is_profile + is_app = False if is_profile else pkg.name in app_names + + if is_profile: + versions = [str(pkg.version)] + elif is_app: + versions = app_vers[:] + else: + versions = [ + str(p.version) + for p in (self.find(pkg.name) if all_vers else [pkg]) + ] - versions = [ - str(p.version) - for p in (self.find(pkg.name) if all_vers_ else [pkg]) - ] packages[pkg.name] = { "package": pkg, "versions": versions, From 983f4faef45020e8cf61461ec190140ca8a8abd7 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 20:59:50 +0800 Subject: [PATCH 25/28] fix startup app updating on patch --- allzpark/control.py | 9 ++++++++- allzpark/delegates.py | 9 +-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index d8e0d5b..6b9741d 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1159,6 +1159,9 @@ def _try_resolve_context(req, pkg_name, mode): contexts = odict() with util.timing() as t: + current_app = self._state["appRequest"] or "" + current_app = current_app.split("==", 1)[0] + for app_request in apps: app_package = _try_finding_latest_app(app_request) @@ -1181,10 +1184,14 @@ def _try_resolve_context(req, pkg_name, mode): app_package.name, mode="Patch") - # update context key `app_request` if patched + # update context key `app_request` if patched, and + # update startup app for pkg in context.resolved_packages or []: if pkg.name == app_package.name: app_request = "%s==%s" % (pkg.name, pkg.version) + if pkg.name == current_app: + self._state.store("startupApplication", + app_request) break contexts[app_request] = context diff --git a/allzpark/delegates.py b/allzpark/delegates.py index b76b7f1..b72502a 100644 --- a/allzpark/delegates.py +++ b/allzpark/delegates.py @@ -53,11 +53,4 @@ def setModelData(self, editor, model, index): if not version or version == default: return - patch_request = "%s==%s" % (package, version) - - for app_pkg in self._ctrl.state["rezApps"].values(): - if app_pkg.name == package: - self._ctrl.state.store("startupApplication", patch_request) - break - - self._ctrl.patch(patch_request) + self._ctrl.patch("%s==%s" % (package, version)) From 7186dc0a1237630e62eab84c3e8b4526f02ff1a4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 21:14:03 +0800 Subject: [PATCH 26/28] fix startup app updating on default --- allzpark/control.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 6b9741d..0d93f5f 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1184,15 +1184,17 @@ def _try_resolve_context(req, pkg_name, mode): app_package.name, mode="Patch") - # update context key `app_request` if patched, and - # update startup app - for pkg in context.resolved_packages or []: - if pkg.name == app_package.name: - app_request = "%s==%s" % (pkg.name, pkg.version) - if pkg.name == current_app: - self._state.store("startupApplication", - app_request) - break + # To avoid application selection change on patched or + # set back to default: + # 1. update context key `app_request`, and + # 2. update startup app + for pkg in context.resolved_packages or []: + if pkg.name == app_package.name: + app_request = "%s==%s" % (pkg.name, pkg.version) + if pkg.name == current_app: + self._state.store("startupApplication", + app_request) + break contexts[app_request] = context From 7f31314281c645f9cc3da0036c738d29cc3d7513 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 21:35:26 +0800 Subject: [PATCH 27/28] only update app_request on success resolve --- allzpark/control.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/allzpark/control.py b/allzpark/control.py index 0d93f5f..1ecd00e 100644 --- a/allzpark/control.py +++ b/allzpark/control.py @@ -1188,13 +1188,14 @@ def _try_resolve_context(req, pkg_name, mode): # set back to default: # 1. update context key `app_request`, and # 2. update startup app - for pkg in context.resolved_packages or []: - if pkg.name == app_package.name: - app_request = "%s==%s" % (pkg.name, pkg.version) - if pkg.name == current_app: - self._state.store("startupApplication", - app_request) - break + if context.success: + for pkg in context.resolved_packages or []: + if pkg.name == app_package.name: + app_request = "%s==%s" % (pkg.name, pkg.version) + if pkg.name == current_app: + self._state.store("startupApplication", + app_request) + break contexts[app_request] = context From b078215ffcc62ede92634b9bcdbcabcf22b80b8a Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 15 Nov 2020 21:56:54 +0800 Subject: [PATCH 28/28] add test --- tests/test_apps.py | 64 ++++++++++++++++++++++++++++++++++++++++++++-- tests/util.py | 19 ++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/tests/test_apps.py b/tests/test_apps.py index 2588242..60e7d95 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -1,5 +1,4 @@ -from unittest import mock from tests import util @@ -223,7 +222,7 @@ def test_app_changing_version(self): }, "app_A": {"1": {"name": "app_A", "version": "1"}}, "app_B": {"1": {"name": "app_B", "version": "1"}, - "2": {"name": "app_B", "version": "2"},} + "2": {"name": "app_B", "version": "2"}} }) self.ctrl_reset(["foo"]) self.show_dock("app") @@ -262,3 +261,64 @@ def get_version_editor(app_request): self.wait(200) # wait patch self.assertEqual("app_B==1", self.ctrl.state["appRequest"]) + + def test_app_no_version_change_if_flattened(self): + """No version edit if versions are flattened with allzparkconfig""" + + def applications_from_package(variant): + # From https://allzpark.com/gui/#multiple-application-versions + from allzpark import _rezapi as rez + + requirements = variant.requires or [] + apps = list( + str(req) + for req in requirements + if req.weak + ) + apps = [rez.PackageRequest(req.strip("~")) for req in apps] + flattened = list() + for request in apps: + flattened += rez.find( + request.name, + range_=request.range, + ) + apps = list( + "%s==%s" % (package.name, package.version) + for package in flattened + ) + return apps + + # patch config + self.patch_allzparkconfig("applications_from_package", + applications_from_package) + # start + util.memory_repository({ + "foo": { + "1": {"name": "foo", "version": "1", + "requires": ["~app_A"]} + }, + "app_A": {"1": {"name": "app_A", "version": "1"}, + "2": {"name": "app_A", "version": "2"}} + }) + self.ctrl_reset(["foo"]) + self.show_dock("app") + + apps = self.window._widgets["apps"] + + def get_version_editor(app_request): + self.select_application(app_request) + proxy = apps.model() + model = proxy.sourceModel() + index = model.findIndex(app_request, column=1) + index = proxy.mapFromSource(index) + apps.edit(index) + + return apps.indexWidget(index), apps.itemDelegate(index) + + editor, delegate = get_version_editor("app_A==1") + self.assertIsNone( + editor, "No version editing if versions are flattened.") + + editor, delegate = get_version_editor("app_A==2") + self.assertIsNone( + editor, "No version editing if versions are flattened.") diff --git a/tests/util.py b/tests/util.py index 538b5a5..75e5e42 100644 --- a/tests/util.py +++ b/tests/util.py @@ -39,6 +39,7 @@ def setUp(self): self.app = app self.ctrl = ctrl self.window = window + self.patched_allzparkconfig = dict() self.wait(timeout=50) @@ -47,8 +48,26 @@ def tearDown(self): self.window.close() self.ctrl.deleteLater() self.window.deleteLater() + self._restore_allzparkconfig() time.sleep(0.1) + def _restore_allzparkconfig(self): + from allzpark import allzparkconfig + + for name, value in self.patched_allzparkconfig.items(): + setattr(allzparkconfig, name, value) + + self.patched_allzparkconfig.clear() + + def patch_allzparkconfig(self, name, value): + from allzpark import allzparkconfig + + if name not in self.patched_allzparkconfig: + original = getattr(allzparkconfig, name) + self.patched_allzparkconfig[name] = original + + setattr(allzparkconfig, name, value) + def set_preference(self, name, value): preferences = self.window._docks["preferences"] arg = next((opt for opt in preferences.options