1# This file is part of MyPaint.
2# Copyright (C) 2013-2018 by the MyPaint Development Team
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9"""String and tuple-based construction and reconstruction of objects."""
10
11## Imports
12
13from __future__ import division, print_function
14import logging
15from warnings import warn
16
17from lib.gibindings import GObject
18
19from lib.observable import event
20
21logger = logging.getLogger(__name__)
22
23
24## Class definitions
25
26class ConstructError (Exception):
27    """Errors encountered when constructing objects.
28
29    Raised when an object cannot be looked up by GType name:
30
31        >>> import gi
32        >>> from lib.gibindings import Gtk
33        >>> make_widget = ObjFactory(gtype=Gtk.Entry)
34        >>> make_widget("NonExist12345")  # doctest: +IGNORE_EXCEPTION_DETAIL
35        Traceback (most recent call last):
36        ConstructError: Cannot construct a 'NonExist12345': module not imp[...]
37
38    Just importing a module defining a class
39    with a "__gtype_name__" defined for it into the running Python interpreter
40    is sufficient to clue GObject's type system
41    into the existence of the class, so the error message refers to that.
42    This exception is also raised when construction
43    fails because the type subclassing requirements are not met:
44
45        >>> make_widget("GtkLabel")  # doctest: +IGNORE_EXCEPTION_DETAIL
46        Traceback (most recent call last):
47        ConstructError: GtkLabel is not a subclass of GtkEntry
48
49    """
50
51
52class ObjFactory (object):
53    """Pythonic cached factory for GObjects.
54
55    Objects are constructable from their GObject type name and a simple tuple
56    containing any construction parameters needed.
57
58      >>> import gi
59      >>> from lib.gibindings import Gtk
60      >>> make_widget = ObjFactory(gtype=Gtk.Widget)
61      >>> w1 = make_widget.get("GtkLabel", "Hello, World",)
62      >>> w1 is not None
63      True
64
65    Factories can be used as functions, given that they basically have only a
66    single job to do.
67
68      >>> w2 = make_widget("GtkLabel", "Hello, World")
69
70    The combination of GObject type name and parameters provides a meaningful
71    identity for a UI element, e.g. a particular window with its own button
72    launcher.  The identifiers are used as a cache key internally.
73
74      >>> w1 is w2
75      True
76
77    Identities can be extracted from objects built by the factory:
78
79      >>> make_widget.identify("constructed elsewhere") is None
80      True
81      >>> saved_ident = make_widget.identify(w1)
82      >>> saved_ident
83      ('GtkLabel', 'Hello, World')
84
85    and allow their reconstruction in future app sessions:
86
87      >>> w3 = make_widget(*saved_ident)
88      >>> w3 is w1
89      True
90
91    """
92
93    def __init__(self, gtype=None):
94        """Constructs, with an optional required type.
95
96        :param gtype: a required type
97        :type gtype: Python GI class representation
98
99        If `gtype` is defined, the factory will be limited to producing objects
100        of that type (or its subclasses) only.
101
102        """
103        super(ObjFactory, self).__init__()
104        self._required_type = gtype
105        self._cache = {}
106
107    def get(self, gtype_name, *params):
108        """Fetch an object by identity, via an internal cache.
109
110        A cache is used, to avoid overconstruction.  If construction is needed,
111        the type name is used to obtain the Python class representing the
112        GObject type, which is then instantiated by passing its Python
113        constructor the supplied parameters as its ``*args``.
114
115        Construction parameters are assumed to qualify and specialize objects
116        sufficiently for `params` plus the type name to form a meaningful
117        identity for the object.
118
119        This is the same concept of identity the cache uses.  If the
120        construction parameters need to change during the lifetime of the
121        object to maintain this identity, the `rebadge()` method can be used to
122        update them and allow the object to be reconstructed correctly for the
123        next session.
124
125        :param gtype_name: a registered name (cf. __gtype_name__)
126        :type gtype_name: str
127        :param params: parameters for the Python constructor
128        :type params: tuple
129        :returns: the newly constructed object
130        :rtype: GObject
131        :raises ConstructError: when construction fails.
132
133        Fires `object_created()` after an object has been successfully created.
134
135        """
136        key = self._make_key(gtype_name, params)
137        if key in self._cache:
138            return self._cache[key]
139        logger.debug("Creating %r via factory", key)
140        try:
141            gtype = GObject.type_from_name(gtype_name)
142        except RuntimeError:
143            raise ConstructError(
144                "Cannot construct a '%s': module not imported?"
145                % gtype_name
146            )
147        if self._required_type:
148            if not gtype.is_a(self._required_type):
149                raise ConstructError(
150                    "%s is not a subclass of %s"
151                    % (gtype_name, self._required_type.__gtype__.name)
152                )
153        try:
154            product = gtype.pytype(*params)
155        except Exception:
156            warn("Failed to construct a %s (pytype=%r, params=%r)"
157                 % (gtype_name, gtype.pytype, params),
158                 RuntimeWarning)
159            raise
160        product.__key = key
161        self._cache[key] = product
162        self.object_created(product)
163        return product
164
165    def __call__(self, gtype_name, *params):
166        """Shorthand allowing use as as a factory pseudo-method."""
167        return self.get(gtype_name, *params)
168
169    @event
170    def object_created(self, product):
171        """Event: an object was created by `get()`
172
173        :param product: The newly constructed object.
174        """
175
176    def cache_has(self, gtype_name, *params):
177        """Returns whether an object with the given key is in the cache.
178
179        :param gtype_name: gtype-system name for the object's class.
180        :param params: Sequence of construction params.
181        :returns: Whether the object with this identity exists in the cache.
182        :rtype: bool
183        """
184        key = self._make_key(gtype_name, params)
185        return key in self._cache
186
187    def identify(self, product):
188        """Gets the typename & params of an object created by this factory.
189
190        :param product: An object created by this factory
191        :returns: identity tuple, or `None`
192        :rtype: None, or a tuple, ``(GTYPENAME, PARAMS...)``
193        """
194        try:
195            key = product.__key
196        except AttributeError:
197            return None
198        return key
199
200    @staticmethod
201    def _make_key(gtype_name, params):
202        """Internal cache key creation function.
203
204        >>> ObjFactory._make_key("GtkLabel", ["test test"])
205        ('GtkLabel', 'test test')
206
207        """
208        return tuple([gtype_name] + list(params))
209
210    def rebadge(self, product, new_params):
211        """Changes the construct params of an object.
212
213        Use this when a constructed object has had something intrinsic changed
214        that's encoded as a construction parameter.
215
216        :params product: An object created by this factory.
217        :params new_params: A new sequence of identifying parameters.
218        :rtype: bool
219        :returns: Whether the rebadge succeeded.
220
221        Rebadging will fail if another object exists in the cache with the same
222        identity.  If successful, this updates the factory cache, and the
223        embedded identifier in the object itself.
224
225        Fires `object_rebadged()` if the parameters were actually changed.
226        Changing the params to their current values has no effect, and does not
227        fire the @event.
228
229        """
230        old_key = self.identify(product)
231        gtype_name = old_key[0]
232        old_params = old_key[1:]
233        new_key = self._make_key(gtype_name, new_params)
234        if old_key == new_key:
235            return True
236        if new_key in self._cache:
237            return False
238        product.__key = new_key
239        self._cache[new_key] = product
240        self._cache.pop(old_key)
241        self.object_rebadged(product, old_params, new_params)
242        return True
243
244    @event
245    def object_rebadged(self, product, old_params, new_params):
246        """Event: object's construct params were updated by `rebadge()`"""
247
248
249if __name__ == '__main__':
250    logging.basicConfig()
251    import doctest
252    doctest.testmod()
253