1#====================== BEGIN GPL LICENSE BLOCK ======================
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17#======================= END GPL LICENSE BLOCK ========================
18
19# <pep8 compliant>
20
21import collections
22
23from types import FunctionType
24from itertools import chain
25
26
27#=============================================
28# Class With Stages
29#=============================================
30
31
32def rigify_stage(stage):
33    """Decorates the method with the specified stage."""
34    def process(method):
35        if not isinstance(method, FunctionType):
36            raise ValueError("Stage decorator must be applied to a method definition")
37        method._rigify_stage = stage
38        return method
39    return process
40
41
42class StagedMetaclass(type):
43    """
44    Metaclass for rigs that manages assignment of methods to stages via @stage.* decorators.
45
46    Using define_stages=True in the class definition will register all non-system
47    method names from that definition as valid stages. After that, subclasses can
48    register methods to those stages, to be called via rigify_invoke_stage.
49    """
50    def __new__(metacls, class_name, bases, namespace, define_stages=None, **kwds):
51        # suppress keyword args to avoid issues with __init_subclass__
52        return super().__new__(metacls, class_name, bases, namespace, **kwds)
53
54    def __init__(self, class_name, bases, namespace, define_stages=None, **kwds):
55        super().__init__(class_name, bases, namespace, **kwds)
56
57        # Compute the set of stages defined by this class
58        if not define_stages:
59            define_stages = []
60
61        elif define_stages is True:
62            define_stages = [
63                name for name, item in namespace.items()
64                if name[0] != '_' and isinstance(item, FunctionType)
65            ]
66
67        self.rigify_own_stages = frozenset(define_stages)
68
69        # Compute complete set of inherited stages
70        staged_bases = [ cls for cls in reversed(self.__mro__) if isinstance(cls, StagedMetaclass) ]
71
72        self.rigify_stages = stages = frozenset(chain.from_iterable(
73            cls.rigify_own_stages for cls in staged_bases
74        ))
75
76        # Compute the inherited stage to method mapping
77        stage_map = collections.defaultdict(collections.OrderedDict)
78        own_stage_map = collections.defaultdict(collections.OrderedDict)
79        method_map = {}
80
81        self.rigify_own_stage_map = own_stage_map
82
83        for base in staged_bases:
84            for stage_name, methods in base.rigify_own_stage_map.items():
85                for method_name, method_class in methods.items():
86                    if method_name in stages:
87                        raise ValueError("Stage method '%s' inherited @stage.%s in class %s (%s)" %
88                                         (method_name, stage_name, class_name, self.__module__))
89
90                    # Check consistency of inherited stage assignment to methods
91                    if method_name in method_map:
92                        if method_map[method_name] != stage_name:
93                            print("RIGIFY CLASS %s (%s): method '%s' has inherited both @stage.%s and @stage.%s\n" %
94                                  (class_name, self.__module__, method_name, method_map[method_name], stage_name))
95                    else:
96                        method_map[method_name] = stage_name
97
98                    stage_map[stage_name][method_name] = method_class
99
100        # Scan newly defined methods for stage decorations
101        for method_name, item in namespace.items():
102            if isinstance(item, FunctionType):
103                stage = getattr(item, '_rigify_stage', None)
104
105                if stage and method_name in stages:
106                    print("RIGIFY CLASS %s (%s): cannot use stage decorator on the stage method '%s' (@stage.%s ignored)" %
107                            (class_name, self.__module__, method_name, stage))
108                    continue
109
110                # Ensure that decorators aren't lost when redefining methods
111                if method_name in method_map:
112                    if not stage:
113                        stage = method_map[method_name]
114                        print("RIGIFY CLASS %s (%s): missing stage decorator on method '%s' (should be @stage.%s)" %
115                              (class_name, self.__module__, method_name, stage))
116                    # Check that the method is assigned to only one stage
117                    elif stage != method_map[method_name]:
118                        print("RIGIFY CLASS %s (%s): method '%s' has decorator @stage.%s, but inherited base has @stage.%s" %
119                              (class_name, self.__module__, method_name, stage, method_map[method_name]))
120
121                # Assign the method to the stage, verifying that it's valid
122                if stage:
123                    if stage not in stages:
124                        raise ValueError("Invalid stage name '%s' for method '%s' in class %s (%s)" %
125                                         (stage, method_name, class_name, self.__module__))
126                    else:
127                        stage_map[stage][method_name] = self
128                        own_stage_map[stage][method_name] = self
129
130        self.rigify_stage_map = stage_map
131
132    def make_stage_decorators(self):
133        return [(name, rigify_stage(name)) for name in self.rigify_stages]
134
135
136class BaseStagedClass(object, metaclass=StagedMetaclass):
137    rigify_sub_objects = tuple()
138
139    def rigify_invoke_stage(self, stage):
140        """Call all methods decorated with the given stage, followed by the callback."""
141        cls = self.__class__
142        assert isinstance(cls, StagedMetaclass)
143        assert stage in cls.rigify_stages
144
145        getattr(self, stage)()
146
147        for sub in self.rigify_sub_objects:
148            sub.rigify_invoke_stage(stage)
149
150        for method_name in cls.rigify_stage_map[stage]:
151            getattr(self, method_name)()
152
153
154#=============================================
155# Per-owner singleton class
156#=============================================
157
158
159class SingletonPluginMetaclass(StagedMetaclass):
160    """Metaclass for maintaining one instance per owner object per constructor arg set."""
161    def __call__(cls, owner, *constructor_args):
162        key = (cls, *constructor_args)
163        try:
164            return owner.plugin_map[key]
165        except KeyError:
166            new_obj = super().__call__(owner, *constructor_args)
167            owner.plugin_map[key] = new_obj
168            owner.plugin_list.append(new_obj)
169            owner.plugin_list.sort(key=lambda obj: obj.priority, reverse=True)
170            return new_obj
171