Skip to content

validate_clashing_sibling_names

ValidateClashingSiblingNames

Bases: MayaInstancePlugin, OptionalPyblishPluginMixin

Validate siblings have unique names when namespaces are stripped.

Source code in client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class ValidateClashingSiblingNames(plugin.MayaInstancePlugin,
                                   OptionalPyblishPluginMixin):
    """Validate siblings have unique names when namespaces are stripped."""

    order = ValidateContentsOrder
    families = ["pointcache", "animation", "usd"]
    label = "Validate clashing sibling names"
    actions = [ayon_maya.api.action.SelectInvalidAction]

    @staticmethod
    def get_invalid(instance):
        """Return all nodes that have non-unique names with siblings when
        namespaces are stripped.

        Returns:
            list[str]: Non-unique siblings
        """
        stripped_name_to_full_path = defaultdict(set)
        for node in instance:
            stripped_name = remove_namespace(node)
            stripped_name_to_full_path[stripped_name].add(node)

        invalid: "list[str]" = []
        for _stripped_name, nodes in stripped_name_to_full_path.items():
            if len(nodes) > 1:
                invalid.extend(nodes)

        if invalid:
            # We only care about the highest conflicts since child conflicts
            # only occur due to the conflicts higher up anyway
            invalid = lib.get_highest_in_hierarchy(invalid)

        return invalid

    def process(self, instance):
        """Process all the nodes in the instance "objectSet"""
        if not self.is_active(instance.data):
            return

        if "publish_attributes" not in instance.data:
            # This is an instance generated at runtime, for example a USD
            # contribution workflow instance. We will ignore this validation
            self.log.debug("Skipping validation of clashing siblings for"
                           f" '{instance}', because it is a runtime instance"
                           " (has no publish attributes)")
            return

        if not self.is_strip_namespaces_enabled(instance):
            return

        invalid = self.get_invalid(instance)
        if invalid:

            report_list = "\n".join(f"- {node}" for node in sorted(invalid))

            raise PublishValidationError(
                "With stripped namespaces there are conflicting sibling names "
                "that are not unique:\n"
                f"{report_list}",
                description=self.get_description())

    def is_strip_namespaces_enabled(self, instance) -> bool:
        """Return whether any extractor is enabled for instance that has
        `stripNamespaces` enabled."""
        # TODO: Preferably there would be a better way to detect whether the
        #   flag was enabled or not.

        plugins = instance.context.data["create_context"].publish_plugins
        plugins_by_name = {plugin.__name__: plugin for plugin in plugins}

        def _is_plugin_active(plugin_name: str) -> bool:
            """Return whether plugin is active for instance"""
            # Check if Plug-in is found
            plugin = plugins_by_name.get(plugin_name)
            if not plugin:
                self.log.debug(f"Plugin {plugin_name} not found. "
                               "It may be disabled in settings")
                return False

            # Check if plug-in is globally enabled
            if not getattr(plugin, "enabled", True):
                self.log.debug(f"Plugin {plugin_name} is disabled. "
                               "It is disabled in settings")
                return False

            # Check if optional state has active state set to False
            publish_attributes = instance.data["publish_attributes"]
            default_active = getattr(plugin, "active", True)
            active_for_instance = publish_attributes.get(
                plugin_name, {}).get("active", default_active)
            if not active_for_instance:
                self.log.debug(
                  f"Plugin {plugin_name} is disabled for this instance.")
                return False

            # Check if the instance, according to pyblish is a match for the
            # plug-in. This may e.g. be excluded due to different families
            # or matching algorithm (e.g. ExtractMultiverseUsdAnim uses
            # `pyblish.api.Subset`
            if not pyblish.api.instances_by_plugin([instance], plugin):
                self.log.debug(
                    f"Plugin {plugin_name} does not match for this instance.")
                return False

            return True

        for plugin_name in [
            "ExtractAlembic",               # pointcache
            "ExtractAnimation",             # animation
            "ExtractMayaUsd",               # usd
            "ExtractMayaUsdPointcache",     # pointcache
            "ExtractMayaUsdAnim",           # animation
        ]:
            if _is_plugin_active(plugin_name):
                plugin = plugins_by_name[plugin_name]

                # Use the value from the instance publish attributes
                publish_attributes = instance.data["publish_attributes"]
                strip_namespaces = publish_attributes.get(
                    plugin_name, {}).get("stripNamespaces")
                if strip_namespaces:
                    return True

                # Find some default on the plugin class, if any
                default = getattr(plugin, "stripNamespaces", False)
                if default:
                    self.log.debug(
                        f"{plugin_name} has strip namespaces enabled as "
                        "default value.")
                    return True
        return False

    def get_description(self):
        return inspect.cleandoc("""
            ### Clashing sibling names with stripped namespaces

            The export has **strip namespaces** enabled but a conflict on 
            sibling names are found where, without namespaces, they do not have
            unique names and can not be exported.

            To resolve this, either export with 'strip namespaces' disabled or
            reorder the hierarchy so that nodes sharing the parent do not have
            the same name.
        """)

get_invalid(instance) staticmethod

Return all nodes that have non-unique names with siblings when namespaces are stripped.

Returns:

Type Description

list[str]: Non-unique siblings

Source code in client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@staticmethod
def get_invalid(instance):
    """Return all nodes that have non-unique names with siblings when
    namespaces are stripped.

    Returns:
        list[str]: Non-unique siblings
    """
    stripped_name_to_full_path = defaultdict(set)
    for node in instance:
        stripped_name = remove_namespace(node)
        stripped_name_to_full_path[stripped_name].add(node)

    invalid: "list[str]" = []
    for _stripped_name, nodes in stripped_name_to_full_path.items():
        if len(nodes) > 1:
            invalid.extend(nodes)

    if invalid:
        # We only care about the highest conflicts since child conflicts
        # only occur due to the conflicts higher up anyway
        invalid = lib.get_highest_in_hierarchy(invalid)

    return invalid

is_strip_namespaces_enabled(instance)

Return whether any extractor is enabled for instance that has stripNamespaces enabled.

Source code in client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def is_strip_namespaces_enabled(self, instance) -> bool:
    """Return whether any extractor is enabled for instance that has
    `stripNamespaces` enabled."""
    # TODO: Preferably there would be a better way to detect whether the
    #   flag was enabled or not.

    plugins = instance.context.data["create_context"].publish_plugins
    plugins_by_name = {plugin.__name__: plugin for plugin in plugins}

    def _is_plugin_active(plugin_name: str) -> bool:
        """Return whether plugin is active for instance"""
        # Check if Plug-in is found
        plugin = plugins_by_name.get(plugin_name)
        if not plugin:
            self.log.debug(f"Plugin {plugin_name} not found. "
                           "It may be disabled in settings")
            return False

        # Check if plug-in is globally enabled
        if not getattr(plugin, "enabled", True):
            self.log.debug(f"Plugin {plugin_name} is disabled. "
                           "It is disabled in settings")
            return False

        # Check if optional state has active state set to False
        publish_attributes = instance.data["publish_attributes"]
        default_active = getattr(plugin, "active", True)
        active_for_instance = publish_attributes.get(
            plugin_name, {}).get("active", default_active)
        if not active_for_instance:
            self.log.debug(
              f"Plugin {plugin_name} is disabled for this instance.")
            return False

        # Check if the instance, according to pyblish is a match for the
        # plug-in. This may e.g. be excluded due to different families
        # or matching algorithm (e.g. ExtractMultiverseUsdAnim uses
        # `pyblish.api.Subset`
        if not pyblish.api.instances_by_plugin([instance], plugin):
            self.log.debug(
                f"Plugin {plugin_name} does not match for this instance.")
            return False

        return True

    for plugin_name in [
        "ExtractAlembic",               # pointcache
        "ExtractAnimation",             # animation
        "ExtractMayaUsd",               # usd
        "ExtractMayaUsdPointcache",     # pointcache
        "ExtractMayaUsdAnim",           # animation
    ]:
        if _is_plugin_active(plugin_name):
            plugin = plugins_by_name[plugin_name]

            # Use the value from the instance publish attributes
            publish_attributes = instance.data["publish_attributes"]
            strip_namespaces = publish_attributes.get(
                plugin_name, {}).get("stripNamespaces")
            if strip_namespaces:
                return True

            # Find some default on the plugin class, if any
            default = getattr(plugin, "stripNamespaces", False)
            if default:
                self.log.debug(
                    f"{plugin_name} has strip namespaces enabled as "
                    "default value.")
                return True
    return False

process(instance)

Process all the nodes in the instance "objectSet

Source code in client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def process(self, instance):
    """Process all the nodes in the instance "objectSet"""
    if not self.is_active(instance.data):
        return

    if "publish_attributes" not in instance.data:
        # This is an instance generated at runtime, for example a USD
        # contribution workflow instance. We will ignore this validation
        self.log.debug("Skipping validation of clashing siblings for"
                       f" '{instance}', because it is a runtime instance"
                       " (has no publish attributes)")
        return

    if not self.is_strip_namespaces_enabled(instance):
        return

    invalid = self.get_invalid(instance)
    if invalid:

        report_list = "\n".join(f"- {node}" for node in sorted(invalid))

        raise PublishValidationError(
            "With stripped namespaces there are conflicting sibling names "
            "that are not unique:\n"
            f"{report_list}",
            description=self.get_description())

remove_namespace(path)

Remove namespace from full path.

Example

remove_namespace("|aa:bb:foo|aa:bb:bar|cc:hello|dd:world") '|foo|bar|hello|world'

Parameters:

Name Type Description Default
path str

Full node path.

required

Returns:

Name Type Description
str str

Node path with namespaces removed.

Source code in client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def remove_namespace(path: str) -> str:
    """Remove namespace from full path.

    Example:
        >>> remove_namespace("|aa:bb:foo|aa:bb:bar|cc:hello|dd:world")
        '|foo|bar|hello|world'

    Arguments:
        path (str): Full node path.

    Returns:
        str: Node path with namespaces removed.
    """
    return "|".join(
        name.rsplit(":", 1)[-1] for name in path.split("|")
    )