Package flumotion :: Package common :: Module messages
[hide private]

Source Code for Module flumotion.common.messages

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_common_messages -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 21   
 22  """ 
 23  support for serializable translatable messages from component/manager to admin 
 24  """ 
 25   
 26  import time 
 27  import gettext 
 28   
 29  from flumotion.common import log 
 30  from twisted.spread import pb 
 31   
 32  (ERROR, 
 33   WARNING, 
 34   INFO) = range(1, 4) 
 35   
36 -def N_(format):
37 """ 38 Mark a singular string for translation, without translating it. 39 """ 40 return format
41
42 -def ngettext(singular, plural, count):
43 """ 44 Mark a plural string for translation, without translating it. 45 """ 46 return (singular, plural, count)
47
48 -def gettexter(domain):
49 """ 50 Return a function that takes a format string or tuple, and additional 51 format args, 52 and creates a L{Translatable} from it. 53 54 Example:: 55 56 T_ = messages.gettexter('flumotion') 57 t = T_(N_("Could not find '%s'."), file) 58 59 @param domain: the gettext domain to create translatables for. 60 """ 61 def create(format, *args): 62 if isinstance(format, str): 63 return TranslatableSingular(domain, format, *args) 64 else: 65 return TranslatablePlural(domain, format, *args)
66 67 return lambda *args: create(*args) 68
69 -class Translatable(pb.Copyable, pb.RemoteCopy):
70 """ 71 I represent a serializable translatable gettext msg. 72 """ 73 domain = None
74 75 # Taken from twisted.python.util; modified so that if compareAttributes 76 # grows, but we get a message from a remote side that doesn't have one 77 # of the new attributes, that we don't raise an exception
78 -class FancyEqMixin:
79 compareAttributes = ()
80 - def __eq__(self, other):
81 if not self.compareAttributes: 82 return self is other 83 #XXX Maybe get rid of this, and rather use hasattr()s 84 if not isinstance(other, self.__class__): 85 return False 86 for attr in self.compareAttributes: 87 if hasattr(self, attr): 88 if not hasattr(other, attr): 89 return False 90 elif not getattr(self, attr) == getattr(other, attr): 91 return False 92 elif hasattr(other, attr): 93 return False 94 return True
95
96 - def __ne__(self, other):
97 return not self.__eq__(other)
98 99 # NOTE: subclassing FancyEqMixin allows us to compare two 100 # RemoteCopy instances gotten from the same Copyable; this allows 101 # state _append and _remove to work correctly 102 # Take note however that this also means that two RemoteCopy objects 103 # of two different Copyable objects, but with the same args, will 104 # also pass equality 105 # For our purposes, this is fine. 106
107 -class TranslatableSingular(Translatable, FancyEqMixin):
108 """ 109 I represent a translatable gettext msg in the singular form. 110 """ 111 112 compareAttributes = ["domain", "format", "args"] 113
114 - def __init__(self, domain, format, *args):
115 """ 116 @param domain: the text domain for translations of this message 117 @param format: a format string 118 @param args: any arguments to the format string 119 """ 120 self.domain = domain 121 self.format = format 122 self.args = args
123
124 - def defaultMessageId(self):
125 return self.format % self.args
126 pb.setUnjellyableForClass(TranslatableSingular, TranslatableSingular) 127
128 -class TranslatablePlural(Translatable, FancyEqMixin):
129 """ 130 I represent a translatable gettext msg in the plural form. 131 """ 132 133 compareAttributes = ["domain", "singular", "plural", "count", "args"] 134
135 - def __init__(self, domain, format, *args):
136 """ 137 @param domain: the text domain for translations of this message 138 @param format: a (singular, plural, count) tuple 139 @param args: any arguments to the format string 140 """ 141 singular, plural, count = format 142 self.domain = domain 143 self.singular = singular 144 self.plural = plural 145 self.count = count 146 self.args = args
147
148 - def defaultMessageId(self):
149 return self.singular % self.args
150 pb.setUnjellyableForClass(TranslatablePlural, TranslatablePlural) 151
152 -class Translator(log.Loggable):
153 """ 154 I translate translatables and messages. 155 I need to be told where locale directories can be found for all domains 156 I need to translate for. 157 """ 158 159 logCategory = "translator" 160
161 - def __init__(self):
162 self._localedirs = {} # domain name -> list of locale dirs
163
164 - def addLocaleDir(self, domain, dir):
165 """ 166 Add a locale directory for the given text domain. 167 """ 168 if not domain in self._localedirs.keys(): 169 self._localedirs[domain] = [] 170 171 if not dir in self._localedirs[domain]: 172 self.debug('Adding localedir %s for domain %s' % (dir, domain)) 173 self._localedirs[domain].append(dir)
174
175 - def translateTranslatable(self, translatable, lang=None):
176 """ 177 Translate a translatable object, in the given language. 178 179 @param lang: language code (or the current locale if None) 180 """ 181 # gettext.translation objects are rumoured to be cached (API docs) 182 domain = translatable.domain 183 t = None 184 if domain in self._localedirs.keys(): 185 # FIXME: possibly trap IOError and handle nicely ? 186 for localedir in self._localedirs[domain]: 187 try: 188 t = gettext.translation(domain, localedir, lang) 189 except IOError: 190 pass 191 else: 192 self.debug('no locales for domain %s' % domain) 193 194 format = None 195 if not t: 196 # if no translation object found, fall back to C 197 self.debug('no translation found, falling back to C') 198 if isinstance(translatable, TranslatableSingular): 199 format = translatable.format 200 elif isinstance(translatable, TranslatablePlural): 201 if translatable.count == 1: 202 format = translatable.singular 203 else: 204 format = translatable.plural 205 else: 206 raise NotImplementedError('Cannot translate translatable %r' % 207 translatable) 208 else: 209 # translation object found, translate 210 if isinstance(translatable, TranslatableSingular): 211 format = t.gettext(translatable.format) 212 elif isinstance(translatable, TranslatablePlural): 213 format = t.ngettext(translatable.singular, translatable.plural, 214 translatable.count) 215 else: 216 raise NotImplementedError('Cannot translate translatable %r' % 217 translatable) 218 219 if translatable.args: 220 return format % translatable.args 221 else: 222 return format
223
224 - def translate(self, message, lang=None):
225 """ 226 Translate a message, in the given language. 227 """ 228 strings = [] 229 for t in message.translatables: 230 strings.append(self.translateTranslatable(t, lang)) 231 return "".join(strings)
232 233 # NOTE: same caveats apply for FancyEqMixin as above 234 # this might be a little heavy; we could consider only comparing 235 # on id, once we verify that all id's are unique 236
237 -class Message(pb.Copyable, pb.RemoteCopy, FancyEqMixin):
238 """ 239 I am a message to be shown in a UI. 240 """ 241 242 compareAttributes = ["level", "translatables", "debug", "id", "priority", 243 "timestamp"] 244
245 - def __init__(self, level, translatable, debug=None, id=None, priority=50, 246 timestamp=None):
247 """Create a new message. 248 249 The id identifies this kind of message, and serves two purposes. 250 251 The first purpose is to serve as a key by which a kind of 252 message might be removed from a set of messages. For example, a 253 firewire component detecting that a cable has been plugged in 254 will remove any message that the cable is unplugged. 255 256 Secondly it serves so that the message viewers that watch the 257 'current state' of some object only see the latest message of a 258 given type. For example when messages are stored in persistent 259 state objects that can be transferred over the network, it 260 becomes inefficient to store the whole history of status 261 messages. Message stores can keep only the latest message of a 262 given ID. 263 264 @param level: ERROR, WARNING or INFO 265 @param translatable: a translatable possibly with markup for 266 linking to documentation or running commands. 267 @param debug: further, untranslated, debug information, not 268 always shown 269 @param priority: priority compared to other messages of the same 270 level 271 @param timestamp: time since epoch at which the message was 272 generated, in seconds. 273 @param id: A unique id for this kind of message, as 274 discussed above. If not given, will be 275 generated from the contents of the 276 translatable. 277 """ 278 self.level = level 279 self.translatables = [] 280 self.debug = debug 281 self.id = id or translatable.defaultMessageId() 282 self.priority = priority 283 self.timestamp = timestamp or time.time() 284 285 self.add(translatable)
286
287 - def __repr__(self):
288 return '<Message %r at %r>' % (self.id, id(self))
289
290 - def add(self, translatable):
291 if not isinstance(translatable, Translatable): 292 raise ValueError('%r is not Translatable' % translatable) 293 self.translatables.append(translatable)
294 pb.setUnjellyableForClass(Message, Message) 295 296 # these are implemented as factory functions instead of classes because 297 # properly proxying to the correct subclass is hard with Copyable/RemoteCopy
298 -def Error(*args, **kwargs):
299 """ 300 Create a L{Message} at ERROR level, indicating a failure that needs 301 intervention to be resolved. 302 """ 303 return Message(ERROR, *args, **kwargs)
304
305 -def Warning(*args, **kwargs):
306 """ 307 Create a L{Message} at WARNING level, indicating a potential problem. 308 """ 309 return Message(WARNING, *args, **kwargs)
310
311 -def Info(*args, **kwargs):
312 """ 313 Create a L{Message} at INFO level. 314 """ 315 return Message(INFO, *args, **kwargs)
316
317 -class Result(pb.Copyable, pb.RemoteCopy):
318 """ 319 I am used in worker checks to return a result. 320 321 @ivar value: the result value of the check 322 @ivar failed: whether or not the check failed. Typically triggered 323 by adding an ERROR message to the result. 324 @ivar messages: list of messages 325 @type messages: list of L{Message} 326 """
327 - def __init__(self):
328 self.messages = [] 329 self.value = None 330 self.failed = False
331
332 - def succeed(self, value):
333 """ 334 Make the result be successful, setting the given result value. 335 """ 336 self.value = value
337
338 - def add(self, message):
339 """ 340 Add a message to the result. 341 342 @type message: L{Message} 343 """ 344 self.messages.append(message) 345 if message.level == ERROR: 346 self.failed = True 347 self.value = None
348 pb.setUnjellyableForClass(Result, Result) 349