Welcome to mirror list, hosted at ThFree Co, Russian Federation.

metaclass.py « utils « rigify - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 4190510155bca51a4424319ce24f47bf275f6653 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
# SPDX-License-Identifier: GPL-2.0-or-later

import collections

from types import FunctionType
from itertools import chain
from typing import Collection, Callable


##############################################
# Class With Stages
##############################################

def rigify_stage(stage):
    """Decorates the method with the specified stage."""
    def process(method: FunctionType):
        if not isinstance(method, FunctionType):
            raise ValueError("Stage decorator must be applied to a method definition")
        method._rigify_stage = stage
        return method
    return process


class StagedMetaclass(type):
    """
    Metaclass for rigs that manages assignment of methods to stages via @stage.* decorators.

    Using define_stages=True in the class definition will register all non-system
    method names from that definition as valid stages. After that, subclasses can
    register methods to those stages, to be called via rigify_invoke_stage.
    """
    def __new__(mcs, class_name, bases, namespace, define_stages=None, **kwargs):
        # suppress keyword args to avoid issues with __init_subclass__
        return super().__new__(mcs, class_name, bases, namespace, **kwargs)

    def __init__(cls, class_name, bases, namespace, define_stages=None, **kwargs):
        super().__init__(class_name, bases, namespace, **kwargs)

        # Compute the set of stages defined by this class
        if not define_stages:
            define_stages = []

        elif define_stages is True:
            define_stages = [
                name for name, item in namespace.items()
                if name[0] != '_' and isinstance(item, FunctionType)
            ]

        cls.rigify_own_stages = frozenset(define_stages)

        # Compute complete set of inherited stages
        staged_bases = [cls for cls in reversed(cls.__mro__) if isinstance(cls, StagedMetaclass)]

        cls.rigify_stages = stages = frozenset(chain.from_iterable(
            cls.rigify_own_stages for cls in staged_bases
        ))

        # Compute the inherited stage to method mapping
        stage_map = collections.defaultdict(collections.OrderedDict)
        own_stage_map = collections.defaultdict(collections.OrderedDict)
        method_map = {}

        cls.rigify_own_stage_map = own_stage_map

        for base in staged_bases:
            for stage_name, methods in base.rigify_own_stage_map.items():
                for method_name, method_class in methods.items():
                    if method_name in stages:
                        raise ValueError(
                            f"Stage method '{method_name}' inherited @stage.{stage_name} "
                            f"in class {class_name} ({cls.__module__})")

                    # Check consistency of inherited stage assignment to methods
                    if method_name in method_map:
                        if method_map[method_name] != stage_name:
                            print(f"RIGIFY CLASS {class_name} ({cls.__module__}): "
                                  f"method '{method_name}' has inherited both "
                                  f"@stage.{method_map[method_name]} and @stage.{stage_name}\n")
                    else:
                        method_map[method_name] = stage_name

                    stage_map[stage_name][method_name] = method_class

        # Scan newly defined methods for stage decorations
        for method_name, item in namespace.items():
            if isinstance(item, FunctionType):
                stage = getattr(item, '_rigify_stage', None)

                if stage and method_name in stages:
                    print(f"RIGIFY CLASS {class_name} ({cls.__module__}): "
                          f"cannot use stage decorator on the stage method '{method_name}' "
                          f"(@stage.{stage} ignored)")
                    continue

                # Ensure that decorators aren't lost when redefining methods
                if method_name in method_map:
                    if not stage:
                        stage = method_map[method_name]
                        print(f"RIGIFY CLASS {class_name} ({cls.__module__}): "
                              f"missing stage decorator on method '{method_name}' "
                              f"(should be @stage.{stage})")
                    # Check that the method is assigned to only one stage
                    elif stage != method_map[method_name]:
                        print(f"RIGIFY CLASS {class_name} ({cls.__module__}): "
                              f"method '{method_name}' has decorator @stage.{stage}, "
                              f"but inherited base has @stage.{method_map[method_name]}")

                # Assign the method to the stage, verifying that it's valid
                if stage:
                    if stage not in stages:
                        raise ValueError(
                            f"Invalid stage name '{stage}' for method '{method_name}' "
                            f"in class {class_name} ({cls.__module__})")
                    else:
                        stage_map[stage][method_name] = cls
                        own_stage_map[stage][method_name] = cls

        cls.rigify_stage_map = stage_map

    def make_stage_decorators(self) -> list[tuple[str, Callable]]:
        return [(name, rigify_stage(name)) for name in self.rigify_stages]

    def stage_decorator_container(self, cls):
        for name, stage in self.make_stage_decorators():
            setattr(cls, name, stage)
        return cls


class BaseStagedClass(object, metaclass=StagedMetaclass):
    rigify_sub_objects: Collection['BaseStagedClass'] = tuple()
    rigify_sub_object_run_late = False

    def rigify_invoke_stage(self, stage: str):
        """Call all methods decorated with the given stage, followed by the callback."""
        cls = self.__class__
        assert isinstance(cls, StagedMetaclass)
        assert stage in cls.rigify_stages

        getattr(self, stage)()

        for sub in self.rigify_sub_objects:
            if not sub.rigify_sub_object_run_late:
                sub.rigify_invoke_stage(stage)

        for method_name in cls.rigify_stage_map[stage]:
            getattr(self, method_name)()

        for sub in self.rigify_sub_objects:
            if sub.rigify_sub_object_run_late:
                sub.rigify_invoke_stage(stage)


##############################################
# Per-owner singleton class
##############################################

class SingletonPluginMetaclass(StagedMetaclass):
    """Metaclass for maintaining one instance per owner object per constructor arg set."""
    def __call__(cls, owner, *constructor_args):
        key = (cls, *constructor_args)
        try:
            return owner.plugin_map[key]
        except KeyError:
            new_obj = super().__call__(owner, *constructor_args)
            owner.plugin_map[key] = new_obj
            owner.plugin_list.append(new_obj)
            owner.plugin_list.sort(key=lambda obj: obj.priority, reverse=True)
            return new_obj