diff --git a/src/plugins/rv-packages/ocio_source_setup/ocio_source_setup.py b/src/plugins/rv-packages/ocio_source_setup/ocio_source_setup.py index 1a4ef8c2a..ef20f3b5f 100644 --- a/src/plugins/rv-packages/ocio_source_setup/ocio_source_setup.py +++ b/src/plugins/rv-packages/ocio_source_setup/ocio_source_setup.py @@ -5,36 +5,80 @@ # from rv import rvtypes, commands import os +import logging import PyOpenColorIO as OCIO +from functools import partial +from typing import Any, Callable + +logging.basicConfig(format="%(levelname)s: %(message)s") + +package_logger = logging.getLogger("OCIOSourceSetup") + +if "RV_OCIO_SOURCE_SETUP_DEBUG" in os.environ: + package_logger.setLevel(logging.DEBUG) +else: + package_logger.setLevel(logging.INFO) # # Default implementations of helper methods # # -DEFAULT_PIPE = {} +DEFAULT_PIPE: dict[str, list[str]] = {} -DEFAULT_RV_PIPE = { +DEFAULT_RV_PIPE: dict[str, list[str]] = { "RVLinearizePipelineGroup": ["RVLinearize", "RVLensWarp"], "RVLookPipelineGroup": ["RVLookLUT"], "RVDisplayPipelineGroup": ["RVDisplayColor"], } -OCIO_ROLES = {"OCIOFile": "RVLinearizePipelineGroup", "OCIOLook": "RVLookPipelineGroup"} +OCIO_ROLES: dict[str, str] = {"OCIOFile": "RVLinearizePipelineGroup", "OCIOLook": "RVLookPipelineGroup"} + +OCIO_DEFAULTS: dict[str, str] = {} -OCIO_DEFAULTS = {} +METHODS: list[str] = ["ocio_config_from_media", "ocio_node_from_media"] -METHODS = ["ocio_config_from_media", "ocio_node_from_media"] +def ocio_config_from_media(media: str | None, attributes: dict[str, Any] | None) -> OCIO.Config: + """ + Retrieve the current OCIO configuration. + + Args: + media: The media file path (unused in default implementation). + attributes: Additional attributes (unused in default implementation). -def ocio_config_from_media(media, attributes): + Returns: + The current PyOpenColorIO configuration. + + Raises: + Exception: If the OCIO environment variable is not set. + """ if os.getenv("OCIO") is None: raise Exception return OCIO.GetCurrentConfig() -def ocio_node_from_media(config, node, default, media=None, attributes={}): +def ocio_node_from_media( + config: OCIO.Config, node: str, default: list[str], media: str | None = None, attributes: dict[str, Any] | None = None +) -> list[dict[str, Any]]: + """ + Generate the OCIO node pipeline configuration based on the media and context. + + Args: + config: The current OCIO configuration. + node: The node or pipeline group name to evaluate. + default: The default pipeline node types. + media: The media file path. + attributes: Dictionary containing source attributes and default settings. + + Returns: + A list of dictionaries representing the node types, contexts, and properties + required to build the OCIO pipeline. + """ + if attributes is None: + attributes = {} + result = [{"nodeType": d, "context": {}, "properties": {}} for d in default] nodeType = commands.nodeType(node) @@ -55,10 +99,10 @@ def ocio_node_from_media(config, node, default, media=None, attributes={}): ] elif nodeType == "RVLinearizePipelineGroup": - inspace = config.parseColorSpaceFromString(media) - if inspace == "": + inspace = config.parseColorSpaceFromString(media) if media else "" + if not inspace: inspace = attributes.get("default_setting", "") - if inspace != "": + if inspace: result = [ { "nodeType": "OCIOFile", @@ -86,7 +130,7 @@ def ocio_node_from_media(config, node, default, media=None, attributes={}): # "ocio_look.look" : "shot_specific_look"}}] look = attributes.get("default_setting", "") - if look != "": + if look: result = [ { "nodeType": "OCIOLook", @@ -103,111 +147,228 @@ def ocio_node_from_media(config, node, default, media=None, attributes={}): # -def isOCIOManaged(nodeType): - def F(): - try: - managed = commands.getIntProperty("#" + nodeType + ".ocio.active")[0] != 0 - return commands.CheckedMenuState if managed else commands.UncheckedMenuState - except Exception: - return commands.UncheckedMenuState +def _is_ocio_managed(nodeType: str) -> int: + """ + Internal callback logic to determine if a specific node type is currently managed by OCIO. - return F + Args: + nodeType: The node type to check. + Returns: + The RV menu state (CheckedMenuState if managed, UncheckedMenuState otherwise). + """ + try: + managed = commands.getIntProperty(f"#{nodeType}.ocio.active")[0] != 0 + return commands.CheckedMenuState if managed else commands.UncheckedMenuState + except Exception: + return commands.UncheckedMenuState -def isOCIODisplayManaged(group): - def F(): - try: - groupName = "RVDisplayPipelineGroup" - dpipeline = groupMemberOfType(group, groupName) - dOCIO = groupMemberOfType(dpipeline, "OCIODisplay") - managed = commands.getIntProperty(dOCIO + ".ocio.active")[0] != 0 - return commands.CheckedMenuState if managed else commands.UncheckedMenuState - except Exception: - return commands.UncheckedMenuState - return F +def isOCIOManaged(nodeType: str) -> Callable[[], int]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_is_ocio_managed, nodeType=...)`. + """ + return partial(_is_ocio_managed, nodeType=nodeType) -def ocioMenuCheck(nodeType, prop, value): - def F(): - try: - current = commands.getStringProperty("#" + nodeType + "." + prop)[0] - managed = isOCIOManaged(nodeType)() == commands.CheckedMenuState - checked = current == value and managed - return commands.CheckedMenuState if checked else commands.NeutralMenuState - except Exception: - return commands.DisabledMenuState +def _is_ocio_display_managed(group: str) -> int: + """ + Internal callback logic to determine if a display group is currently managed by OCIO. - return F + Args: + group: The display group node name. + Returns: + The RV menu state (CheckedMenuState if managed, UncheckedMenuState otherwise). + """ + try: + groupName = "RVDisplayPipelineGroup" + display_pipeline = groupMemberOfType(group, groupName) + display_ocio = groupMemberOfType(display_pipeline, "OCIODisplay") + managed = commands.getIntProperty(f"{display_ocio}.ocio.active")[0] != 0 + return commands.CheckedMenuState if managed else commands.UncheckedMenuState + except Exception: + return commands.UncheckedMenuState -def ocioDisplayMenuCheck(group, display, view): - def F(): - try: - groupName = "RVDisplayPipelineGroup" - dpipeline = groupMemberOfType(group, groupName) - dOCIO = groupMemberOfType(dpipeline, "OCIODisplay") - d = commands.getStringProperty(dOCIO + ".ocio_display.display")[0] - v = commands.getStringProperty(dOCIO + ".ocio_display.view")[0] - if d == display and v == view: - return commands.CheckedMenuState - return commands.UncheckedMenuState - except Exception: - return commands.DisabledMenuState - return F +def isOCIODisplayManaged(group: str) -> Callable[[], int]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_is_ocio_display_managed, group=...)`. + """ + return partial(_is_ocio_display_managed, group=group) -def ocioEvent(nodeType, prop, value): - "This function will apply its change on the current node of nodeType in the evaluation path" +def _ocio_menu_check(nodeType: str, prop: str, value: str) -> int: + """ + Internal callback logic to determine the menu check state for a specific OCIO property. - def F(event): - commands.setStringProperty("#" + nodeType + "." + prop, [value], True) - commands.redraw() + Args: + nodeType: The OCIO node type. + prop: The property name to check. + value: The value to compare against the current property value. - return F + Returns: + The RV menu state (Checked, Neutral, or Disabled). + """ + try: + current = commands.getStringProperty(f"#{nodeType}.{prop}")[0] + managed = _is_ocio_managed(nodeType) == commands.CheckedMenuState + checked = current == value and managed + return commands.CheckedMenuState if checked else commands.NeutralMenuState + except Exception: + return commands.DisabledMenuState -def ocioEventOnAllOfType(nodeType, prop, value): - "This function will apply its change on all nodes of nodeType" +def ocioMenuCheck(nodeType: str, prop: str, value: str) -> Callable[[], int]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_ocio_menu_check, nodeType=..., prop=..., value=...)`. + """ + return partial(_ocio_menu_check, nodeType=nodeType, prop=prop, value=value) - def F(event): - for node in commands.nodesOfType(nodeType): - commands.setStringProperty(node + "." + prop, [value], True) - commands.redraw() - return F +def _ocio_display_menu_check(group: str, display: str, view: str) -> int: + """ + Internal callback logic to determine the menu check state for a display/view combination. + Args: + group: The display group node name. + display: The OCIO display name. + view: The OCIO view name. -def ocioDisplayEvent(group, display, view): - def F(event): + Returns: + The RV menu state (Checked, Unchecked, or Disabled). + """ + try: groupName = "RVDisplayPipelineGroup" - dpipeline = groupMemberOfType(group, groupName) - dOCIO = groupMemberOfType(dpipeline, "OCIODisplay") - # Both 'display' and 'view' must be set together. - # Disable the OCIONode during display/view propety changes. - # Prevents node from rebuilding shaders while it may be in an invalid state. - commands.setIntProperty(dOCIO + ".ocio.active", [0], True) - commands.setStringProperty(dOCIO + ".ocio_display.display", [display], True) - commands.setStringProperty(dOCIO + ".ocio_display.view", [view], True) - commands.setIntProperty(dOCIO + ".ocio.active", [1], True) - commands.redraw() + display_pipeline = groupMemberOfType(group, groupName) + display_ocio = groupMemberOfType(display_pipeline, "OCIODisplay") + currentDisplay = commands.getStringProperty(f"{display_ocio}.ocio_display.display")[0] + currentView = commands.getStringProperty(f"{display_ocio}.ocio_display.view")[0] + if currentDisplay == display and currentView == view: + return commands.CheckedMenuState + return commands.UncheckedMenuState + except Exception: + return commands.DisabledMenuState + + +def ocioDisplayMenuCheck(group: str, display: str, view: str) -> Callable[[], int]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_ocio_display_menu_check, group=..., display=..., view=...)`. + """ + return partial(_ocio_display_menu_check, group=group, display=display, view=view) + - return F +def _ocio_event(event: Any, nodeType: str, prop: str, value: str) -> None: + """ + Internal callback logic to set a property on the current node of nodeType in the evaluation path. + Args: + event: The RV event object. + nodeType: The OCIO node type. + prop: The property name to set. + value: The value to assign to the property. + """ + commands.setStringProperty(f"#{nodeType}.{prop}", [value], True) + commands.redraw() -def groupMemberOfType(node, memberType): + +def ocioEvent(nodeType: str, prop: str, value: str) -> Callable[[Any], None]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_ocio_event, nodeType=..., prop=..., value=...)`. + Note: The internal `_ocio_event` accepts `event` as its first parameter to allow kwargs binding. + """ + return partial(_ocio_event, nodeType=nodeType, prop=prop, value=value) + + +def _ocio_event_on_all_of_type(event: Any, nodeType: str, prop: str, value: str) -> None: + """ + Internal callback logic to set a property on all nodes of nodeType. + + Args: + event: The RV event object. + nodeType: The OCIO node type. + prop: The property name to set. + value: The value to assign to the property. + """ + for node in commands.nodesOfType(nodeType): + commands.setStringProperty(f"{node}.{prop}", [value], True) + commands.redraw() + + +def ocioEventOnAllOfType(nodeType: str, prop: str, value: str) -> Callable[[Any], None]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_ocio_event_on_all_of_type, nodeType=..., prop=..., value=...)`. + """ + return partial(_ocio_event_on_all_of_type, nodeType=nodeType, prop=prop, value=value) + + +def _ocio_display_event(event: Any, group: str, display: str, view: str) -> None: + """ + Internal callback logic to change the active display and view for a display group. + + Args: + event: The RV event object. + group: The display group node name. + display: The OCIO display name. + view: The OCIO view name. + """ + groupName = "RVDisplayPipelineGroup" + display_pipeline = groupMemberOfType(group, groupName) + display_ocio = groupMemberOfType(display_pipeline, "OCIODisplay") + # Both 'display' and 'view' must be set together. + # Disable the OCIONode during display/view propety changes. + # Prevents node from rebuilding shaders while it may be in an invalid state. + commands.setIntProperty(f"{display_ocio}.ocio.active", [0], True) + commands.setStringProperty(f"{display_ocio}.ocio_display.display", [display], True) + commands.setStringProperty(f"{display_ocio}.ocio_display.view", [view], True) + commands.setIntProperty(f"{display_ocio}.ocio.active", [1], True) + commands.redraw() + + +def ocioDisplayEvent(group: str, display: str, view: str) -> Callable[[Any], None]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(_ocio_display_event, group=..., display=..., view=...)`. + """ + return partial(_ocio_display_event, group=group, display=display, view=view) + + +def groupMemberOfType(node: str, memberType: str) -> str | None: + """ + Find the first member of a group node that matches a specific node type. + + Args: + node: The parent group node name. + memberType: The node type to search for. + + Returns: + The name of the child node if found, otherwise None. + """ for n in commands.nodesInGroup(node): if commands.nodeType(n) == memberType: return n return None -def applyProps(node, contextProps, propertiesProps): +def applyProps(node: str, contextProps: dict[str, str], propertiesProps: dict[str, str]) -> None: + """ + Apply standard and context properties to an OCIO node. + + Args: + node: The target node name. + contextProps: A dictionary of context variables and their values. + propertiesProps: A dictionary of standard properties and their values. + """ for pprop, avalue in propertiesProps.items(): - commands.setStringProperty(node + "." + pprop, [avalue], True) + commands.setStringProperty(f"{node}.{pprop}", [avalue], True) for cprop, cvalue in contextProps.items(): - prop = node + ".ocio_context." + cprop + prop = f"{node}.ocio_context.{cprop}" if not commands.propertyExists(prop): commands.newProperty(prop, commands.StringType, 1) commands.setStringProperty(prop, [cvalue], True) @@ -243,21 +404,26 @@ class OCIOSourceSetupMode(rvtypes.MinorMode): between 0 and 10). """ - def useSourceOCIO(self, source, nodeType, defaultSetting=""): + def useSourceOCIO(self, source: str, nodeType: str, defaultSetting: str = "") -> None: """ This tells the source group to use OCIO instead of the RV linearize node. There is also ocio.look and ocio.preCache which can be activated in this way. For this code we're only assuming that OCIO is going to be used to linearize the source. + + Args: + source: The name of the source group node. + nodeType: The OCIO node type to activate (e.g., 'OCIOFile'). + defaultSetting: The default fallback setting for color space or look. """ - medias = commands.getStringProperty("%s.media.movie" % source) + medias = commands.getStringProperty(f"{source}.media.movie") media = medias[0] try: srcAttrs = commands.sourceAttributes(source, media) - attrDict = dict(zip([i[0] for i in srcAttrs], [j[1] for j in srcAttrs])) + attrDict = {attr[0]: attr[1] for attr in srcAttrs} attrDict["source_node"] = source attrDict["default_setting"] = defaultSetting except Exception: @@ -284,7 +450,7 @@ def useSourceOCIO(self, source, nodeType, defaultSetting=""): if commands.nodeType(pNode).startswith("OCIO"): commands.ocioUpdateConfig(pNode) - print(("INFO: using %s node for %s %s" % (nodeType, source, pipeSlot))) + package_logger.info("using %s node for %s %s", nodeType, source, pipeSlot) return # @@ -306,7 +472,7 @@ def useSourceOCIO(self, source, nodeType, defaultSetting=""): try: if pipeSlot not in DEFAULT_PIPE: - currentPipelineNodes = commands.getStringProperty(srcPipeline + ".pipeline.nodes") + currentPipelineNodes = commands.getStringProperty(f"{srcPipeline}.pipeline.nodes") # We need to handle the following special case here: # We might be in the process of reloading an RV session that @@ -320,19 +486,21 @@ def useSourceOCIO(self, source, nodeType, defaultSetting=""): DEFAULT_PIPE[pipeSlot] = currentPipelineNodes pipelineList = ocio_node_from_media(self.config, srcPipeline, DEFAULT_PIPE[pipeSlot], media, attrDict) except Exception as inst: - print(("ERROR: Problem occurred while loading OCIO settings for %s: %s" % (nodeType, inst))) + package_logger.error("Problem occurred while loading OCIO settings for %s: %s", nodeType, inst) return try: pipeline = [p["nodeType"] for p in pipelineList] except KeyError as inst: - print(("ERROR: Unable to make use of ocio_node_from_media return: %s" % inst)) + package_logger.error("Unable to make use of ocio_node_from_media return: %s", inst) + return + if pipeline == DEFAULT_PIPE[pipeSlot]: return - print(("INFO: using %s node for %s %s" % (nodeType, source, pipeSlot))) + package_logger.info("using %s node for %s %s", nodeType, source, pipeSlot) - commands.setStringProperty(srcPipeline + ".pipeline.nodes", pipeline, True) + commands.setStringProperty(f"{srcPipeline}.pipeline.nodes", pipeline, True) pipeNodes = commands.nodesInGroup(srcPipeline) pipeNodes.sort() for index, pNode in enumerate(pipelineList): @@ -340,30 +508,34 @@ def useSourceOCIO(self, source, nodeType, defaultSetting=""): try: applyProps(stageOCIO, pNode["context"], pNode["properties"]) except KeyError as inst: - print(("ERROR: Unable to apply properties to %s: %s" % (stageOCIO, inst))) + package_logger.error("Unable to apply properties to %s: %s", stageOCIO, inst) commands.redraw() - def disableSourceOCIO(self, source, nodeType): + def disableSourceOCIO(self, source: str, nodeType: str) -> None: """ This reverts the source group's linearize node back to using a native RVLinearize node. + + Args: + source: The name of the source group node. + nodeType: The OCIO node type being disabled. """ pipeSlot = OCIO_ROLES[nodeType] srcPipeline = groupMemberOfType(commands.nodeGroup(source), pipeSlot) - nodesProp = srcPipeline + ".pipeline.nodes" + nodesProp = f"{srcPipeline}.pipeline.nodes" current = commands.getStringProperty(nodesProp) if pipeSlot not in DEFAULT_PIPE or current == DEFAULT_PIPE[pipeSlot]: return - print(("INFO: resetting %s for %s" % (pipeSlot, source))) + package_logger.info("resetting %s for %s", pipeSlot, source) - commands.setStringProperty(srcPipeline + ".pipeline.nodes", DEFAULT_PIPE[pipeSlot], True) + commands.setStringProperty(f"{srcPipeline}.pipeline.nodes", DEFAULT_PIPE[pipeSlot], True) commands.redraw() - def useDisplayOCIO(self, group): + def useDisplayOCIO(self, group: str) -> None: """ This installs the OCIODisplay node in the DisplayGroup's display pipeline in place of RV's RVDisplayColor node. @@ -371,6 +543,9 @@ def useDisplayOCIO(self, group): NOTE: in RV4 all display devices are separate DisplayGroups. So each one can have a completely different view and display transform. + + Args: + group: The display group node name. """ if self.usingOCIOForDisplay.get(group, False) or self.config is None: @@ -378,9 +553,9 @@ def useDisplayOCIO(self, group): groupName = "RVDisplayPipelineGroup" try: - dpipeline = groupMemberOfType(group, groupName) + display_pipeline = groupMemberOfType(group, groupName) if groupName not in DEFAULT_PIPE: - currentPipelineNodes = commands.getStringProperty(dpipeline + ".pipeline.nodes") + currentPipelineNodes = commands.getStringProperty(f"{display_pipeline}.pipeline.nodes") # We need to handle the following special case here: # We might be in the process of reloading an RV session that @@ -391,64 +566,71 @@ def useDisplayOCIO(self, group): DEFAULT_PIPE[groupName] = DEFAULT_RV_PIPE[groupName] else: DEFAULT_PIPE[groupName] = currentPipelineNodes - pipelineList = ocio_node_from_media(self.config, dpipeline, DEFAULT_PIPE[groupName]) + pipelineList = ocio_node_from_media(self.config, display_pipeline, DEFAULT_PIPE[groupName]) except Exception as inst: - print(("ERROR: Problem occurred while loading OCIO settings for OCIODisplay: %s" % inst)) + package_logger.error("Problem occurred while loading OCIO settings for OCIODisplay: %s", inst) return try: pipeline = [p["nodeType"] for p in pipelineList] except KeyError as inst: - print(("ERROR: Unable to make use of ocio_node_from_media return: %s" % inst)) + package_logger.error("Unable to make use of ocio_node_from_media return: %s", inst) + return + if pipeline == DEFAULT_PIPE[groupName]: return - device = commands.getStringProperty(group + ".device.name")[0] - print(("INFO: using OCIODisplay for display: %s" % device)) + device = commands.getStringProperty(f"{group}.device.name")[0] + package_logger.info("using OCIODisplay for display: %s", device) - dpipeline = groupMemberOfType(group, groupName) - commands.setStringProperty(dpipeline + ".pipeline.nodes", pipeline, True) + commands.setStringProperty(f"{display_pipeline}.pipeline.nodes", pipeline, True) - pipeNodes = commands.nodesInGroup(dpipeline) + pipeNodes = commands.nodesInGroup(display_pipeline) pipeNodes.sort() for index, pNode in enumerate(pipelineList): stageOCIO = pipeNodes[index] try: applyProps(stageOCIO, pNode["context"], pNode["properties"]) except KeyError as inst: - print(("ERROR: Unable to apply properties to %s: %s" % (stageOCIO, inst))) + package_logger.error("Unable to apply properties to %s: %s", stageOCIO, inst) self.usingOCIOForDisplay[group] = True commands.redraw() - def disableDisplayOCIO(self, group): + def disableDisplayOCIO(self, group: str) -> None: """ This reverts the DisplayGroup's display pipeline back to using RV's native RVDisplayColor node. + + Args: + group: The display group node name. """ groupName = "RVDisplayPipelineGroup" - dpipeline = groupMemberOfType(group, groupName) - nodesProp = dpipeline + ".pipeline.nodes" + display_pipeline = groupMemberOfType(group, groupName) + nodesProp = f"{display_pipeline}.pipeline.nodes" current = commands.getStringProperty(nodesProp) if groupName not in DEFAULT_PIPE or current == DEFAULT_PIPE[groupName]: return - commands.setStringProperty(dpipeline + ".pipeline.nodes", DEFAULT_PIPE[groupName], True) + commands.setStringProperty(f"{display_pipeline}.pipeline.nodes", DEFAULT_PIPE[groupName], True) - device = commands.getStringProperty(group + ".device.name")[0] - print(("INFO: using RVDisplayColor for display: %s" % device)) + device = commands.getStringProperty(f"{group}.device.name")[0] + package_logger.info("using RVDisplayColor for display: %s", device) self.usingOCIOForDisplay[group] = False commands.redraw() - def sourceSetup(self, event): + def sourceSetup(self, event: Any) -> None: """ This function should be bound to the "source-group-complete" event. It will attempt to use OCIO to infer the incoming file space. If it succeeds, the OCIOFile node of the source group is activated and used to convert to the ROLE_SCENE_LINEAR space. + + Args: + event: The RV event object triggering the setup. """ event.reject() # don't eat this event -- allow others to get it too @@ -457,9 +639,9 @@ def sourceSetup(self, event): group = args[0] fileSource = groupMemberOfType(group, "RVFileSource") imageSource = groupMemberOfType(group, "RVImageSource") - source = fileSource if imageSource is None else imageSource + source = imageSource or fileSource - for nodeType in OCIO_ROLES.keys(): + for nodeType in OCIO_ROLES: self.useSourceOCIO(source, nodeType) # @@ -469,15 +651,27 @@ def sourceSetup(self, event): # if len(commands.nodesOfType("OCIOFile")) == 1: - for group in commands.nodesOfType("RVDisplayGroup"): - if not self.usingOCIOForDisplay.get(group, False): - self.useDisplayOCIO(group) + for dgroup in commands.nodesOfType("RVDisplayGroup"): + if not self.usingOCIOForDisplay.get(dgroup, False): + self.useDisplayOCIO(dgroup) + + def beforeSessionRead(self, event: Any) -> None: + """ + Flag that a session is currently being read. - def beforeSessionRead(self, event): + Args: + event: The RV event object. + """ event.reject() self.readingSession = True - def afterSessionRead(self, event): + def afterSessionRead(self, event: Any) -> None: + """ + Clear the session read flag and re-initialize OCIO display if needed. + + Args: + event: The RV event object. + """ event.reject() self.readingSession = False if len(commands.nodesOfType("OCIOFile")) > 1: @@ -485,30 +679,47 @@ def afterSessionRead(self, event): if not self.usingOCIOForDisplay.get(group, False): self.useDisplayOCIO(group) - def ocioActiveEvent(self, nodeType): - def F(event): - if nodeType not in ["OCIOFile", "OCIOLook"]: - if isOCIODisplayManaged(nodeType)() == commands.CheckedMenuState: - self.disableDisplayOCIO(nodeType) - else: - self.useDisplayOCIO(nodeType) - return - - evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVFileSource", None) - if len(evalInfo) == 0: - evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVImageSource", None) - if len(evalInfo) == 0: - return - source = evalInfo[0]["node"] + def _ocio_active_event(self, event: Any, nodeType: str) -> None: + """ + Toggle the active state of an OCIO node or display group. - if isOCIOManaged(nodeType)() == commands.CheckedMenuState: - self.disableSourceOCIO(source, nodeType) + Args: + event: The RV event object. + nodeType: The OCIO node type or display group to toggle. + """ + if nodeType not in ["OCIOFile", "OCIOLook"]: + if _is_ocio_display_managed(nodeType) == commands.CheckedMenuState: + self.disableDisplayOCIO(nodeType) else: - self.useSourceOCIO(source, nodeType, OCIO_DEFAULTS[nodeType]) + self.useDisplayOCIO(nodeType) + return + + evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVFileSource", None) + if not evalInfo: + evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVImageSource", None) + if not evalInfo: + return + source = evalInfo[0]["node"] - return F + if _is_ocio_managed(nodeType) == commands.CheckedMenuState: + self.disableSourceOCIO(source, nodeType) + else: + self.useSourceOCIO(source, nodeType, OCIO_DEFAULTS[nodeType]) - def checkForDisplayGroup(self, event): + def ocioActiveEvent(self, nodeType: str) -> Callable[[Any], None]: + """ + Deprecated: Public API maintained for backward compatibility. + Internal code should use `functools.partial(self._ocio_active_event, nodeType=...)`. + """ + return partial(self._ocio_active_event, nodeType=nodeType) + + def checkForDisplayGroup(self, event: Any) -> None: + """ + Check for newly created or modified display groups and rebuild the menu. + + Args: + event: The RV event object. + """ event.reject() try: node = event.contents() @@ -516,26 +727,38 @@ def checkForDisplayGroup(self, event): self.usingOCIOForDisplay[node] = False commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True) except Exception as inst: - print((str(inst), node)) + package_logger.error("%s %s", inst, node) + + def maybeUpdateViews(self, event: Any) -> None: + """ + Rebuild the OCIO menu if a display view has changed. - def maybeUpdateViews(self, event): + Args: + event: The RV event object. + """ event.reject() if event.contents().endswith("ocio_display.display"): commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True) - def selectConfig(self, event): + def selectConfig(self, event: Any) -> None: + """ + Prompt the user to manually select an OCIO configuration file. + + Args: + event: The RV event object. + """ try: config = commands.openFileDialog(True, False, False, "ocio|OCIO Config", None)[0] self.config = OCIO.Config.CreateFromFile(config) OCIO.SetCurrentConfig(self.config) for source in commands.nodesOfType("RVFileSource") + commands.nodesOfType("RVImageSource"): - for nodeType in OCIO_ROLES.keys(): + for nodeType in OCIO_ROLES: self.disableSourceOCIO(source, nodeType) for group in commands.nodesOfType("RVDisplayGroup"): self.disableDisplayOCIO(group) DEFAULT_PIPE.clear() for source in commands.nodesOfType("RVFileSource") + commands.nodesOfType("RVImageSource"): - for nodeType in OCIO_ROLES.keys(): + for nodeType in OCIO_ROLES: self.useSourceOCIO(source, nodeType) for group in commands.nodesOfType("RVDisplayGroup"): self.usingOCIOForDisplay[group] = False @@ -543,9 +766,15 @@ def selectConfig(self, event): commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True) commands.writeSettings("ocio_source_setup", "ocio_config", config) except Exception as inst: - print(inst) + package_logger.error(inst) - def buildOCIOMenu(self): + def buildOCIOMenu(self) -> list[tuple[str, list[Any]]]: + """ + Construct the RV menu items required for OCIO management. + + Returns: + A list defining the OCIO menu structure. + """ # # Try to acquire OCIO config to populate the display menu # @@ -566,9 +795,9 @@ def buildOCIOMenu(self): dList = [ ( "Active", - self.ocioActiveEvent(display), + partial(self._ocio_active_event, nodeType=display), None, - isOCIODisplayManaged(display), + partial(_is_ocio_display_managed, group=display), ), ("_", None), ] @@ -578,70 +807,73 @@ def buildOCIOMenu(self): vList.append( ( v, - ocioDisplayEvent(display, d, v), + partial(_ocio_display_event, group=display, display=d, view=v), None, - ocioDisplayMenuCheck(display, d, v), + partial(_ocio_display_menu_check, group=display, display=d, view=v), ) ) dList.append((d, vList)) - device = " " + commands.getStringProperty(display + ".device.name")[0] + device_name = commands.getStringProperty(f"{display}.device.name")[0] + device = f" {device_name}" daList.append((device, dList)) # # Apply file space changes only to the visible source # - cssList = [ + cssList: list[Any] = [ ( "Active", - self.ocioActiveEvent("OCIOFile"), + partial(self._ocio_active_event, nodeType="OCIOFile"), None, - isOCIOManaged("OCIOFile"), + partial(_is_ocio_managed, nodeType="OCIOFile"), ), ("_", None), ] - csaList = [] + csaList: list[Any] = [] - def addPath(family, tree): + def addPath(family: list[str], tree: list[list[str]]) -> None: for f in family: for t in tree: if f in t: - return addPath(family[1:], t) + addPath(family[1:], t) + return tree.append([f]) - return addPath(family, tree) + addPath(family, tree) + return families = [(cs.getFamily().split("/") + [cs.getName()]) for cs in self.config.getColorSpaces()] - root = [] + root: list[list[str]] = [] for family in families: addPath(family, root) - def addMenu(root, isSingle): - if len(root) == 1: - name = root[0] + def addMenu(root_node: list[Any], isSingle: bool) -> list[Any]: + if len(root_node) == 1: + name = root_node[0] if isSingle: OCIO_DEFAULTS.setdefault("OCIOFile", name) return [ ( name, - ocioEvent("OCIOFile", "ocio.inColorSpace", name), + partial(_ocio_event, nodeType="OCIOFile", prop="ocio.inColorSpace", value=name), None, - ocioMenuCheck("OCIOFile", "ocio.inColorSpace", name), + partial(_ocio_menu_check, nodeType="OCIOFile", prop="ocio.inColorSpace", value=name), ) ] else: return [ ( name, - ocioEventOnAllOfType("OCIOFile", "ocio.inColorSpace", name), + partial(_ocio_event_on_all_of_type, nodeType="OCIOFile", prop="ocio.inColorSpace", value=name), None, - ocioMenuCheck("OCIOFile", "ocio.inColorSpace", name), + partial(_ocio_menu_check, nodeType="OCIOFile", prop="ocio.inColorSpace", value=name), ) ] else: menu = [] - for r in root[1:]: + for r in root_node[1:]: menu += addMenu(r, isSingle) - return [(root[0], menu)] + return [(root_node[0], menu)] for r in root: cssList += addMenu(r, True) @@ -651,36 +883,36 @@ def addMenu(root, isSingle): # Apply file look changes only to the visible source # - lsList = [ + lsList: list[Any] = [ ( "Active", - self.ocioActiveEvent("OCIOLook"), + partial(self._ocio_active_event, nodeType="OCIOLook"), None, - isOCIOManaged("OCIOLook"), + partial(_is_ocio_managed, nodeType="OCIOLook"), ), ("_", None), ] - laList = [] + laList: list[Any] = [] for look in self.config.getLooks(): OCIO_DEFAULTS.setdefault("OCIOLook", look.getName()) lsList.append( ( look.getName(), - ocioEvent("OCIOLook", "ocio_look.look", look.getName()), + partial(_ocio_event, nodeType="OCIOLook", prop="ocio_look.look", value=look.getName()), None, - ocioMenuCheck("OCIOLook", "ocio_look.look", look.getName()), + partial(_ocio_menu_check, nodeType="OCIOLook", prop="ocio_look.look", value=look.getName()), ) ) laList.append( ( look.getName(), - ocioEventOnAllOfType("OCIOLook", "ocio_look.look", look.getName()), + partial(_ocio_event_on_all_of_type, nodeType="OCIOLook", prop="ocio_look.look", value=look.getName()), None, - ocioMenuCheck("OCIOLook", "ocio_look.look", look.getName()), + partial(_ocio_menu_check, nodeType="OCIOLook", prop="ocio_look.look", value=look.getName()), ) ) - final = [ + final: list[Any] = [ ("Current Source", None, None, lambda: commands.DisabledMenuState), (" File Color Space", cssList), ] @@ -702,12 +934,16 @@ def addMenu(root, isSingle): return [("OCIO", final)] - def __init__(self): - rvtypes.MinorMode.__init__(self) + def __init__(self) -> None: + """ + Initialize the minor mode, attempt to load inherited configuration, + and bind the mode events to RV. + """ + super().__init__() - self.usingOCIOForDisplay = {} - self.readingSession = False - self.config = None + self.usingOCIOForDisplay: dict[str, bool] = {} + self.readingSession: bool = False + self.config: OCIO.Config | None = None # # Look for an implementation of the OCIOHelper on the PATH. @@ -720,12 +956,13 @@ def __init__(self): inherited = [] for method in METHODS: try: - exec("global %s; %s = rv_ocio_setup.%s" % (method, method, method)) + override_method = getattr(rv_ocio_setup, method) + globals()[method] = override_method inherited.append(method) except AttributeError: pass - print(("INFO: Using %s for OCIO setup methods: %s" % (rv_ocio_setup.__file__, " ".join(inherited)))) + package_logger.info("Using %s for OCIO setup methods: %s", rv_ocio_setup.__file__, " ".join(inherited)) except ImportError: pass @@ -734,11 +971,11 @@ def __init__(self): # An externally set OCIO env var takes precendence if os.getenv("OCIO") is None: config = commands.readSettings("ocio_source_setup", "ocio_config", "") - if config != "" and os.path.isfile(config): + if config and os.path.isfile(config): self.config = OCIO.Config.CreateFromFile(config) OCIO.SetCurrentConfig(self.config) else: - print("WARNING: $OCIO environment variable unset!") + package_logger.warning("$OCIO environment variable unset!") self.init( "OCIO Source Setup", @@ -767,5 +1004,11 @@ def __init__(self): # -def createMode(): +def createMode() -> OCIOSourceSetupMode: + """ + Factory function used by the RV Mode Manager to instantiate the mode. + + Returns: + An instance of OCIOSourceSetupMode. + """ return OCIOSourceSetupMode()