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