[ADD] tools: handling of bin_path key in find_in_path
[odoo/odoo.git] / openerp / tools / misc.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23
24 """
25 Miscellaneous tools used by OpenERP.
26 """
27
28 from functools import wraps
29 import cProfile
30 from contextlib import contextmanager
31 import subprocess
32 import logging
33 import os
34 import socket
35 import sys
36 import threading
37 import time
38 import werkzeug.utils
39 import zipfile
40 from collections import defaultdict, Mapping
41 from datetime import datetime
42 from itertools import islice, izip, groupby
43 from lxml import etree
44 from which import which
45 from threading import local
46 import traceback
47
48 try:
49     from html2text import html2text
50 except ImportError:
51     html2text = None
52
53 from config import config
54 from cache import *
55 from .parse_version import parse_version 
56
57 import openerp
58 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
59 # There are moved to loglevels until we refactor tools.
60 from openerp.loglevels import get_encodings, ustr, exception_to_unicode     # noqa
61
62 _logger = logging.getLogger(__name__)
63
64 # List of etree._Element subclasses that we choose to ignore when parsing XML.
65 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
66 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
67
68 DEFAULT_PATH = os.environ.get('PATH', os.defpath).split(os.pathsep)
69 if config.get('bin_path'):
70     DEFAULT_PATH.append(config['bin_path'])
71
72 def find_in_path(name):
73     try:
74         return which(name, path=os.pathsep.join(DEFAULT_PATH))
75     except IOError:
76         return None
77
78 def find_pg_tool(name):
79     path = None
80     if config['pg_path'] and config['pg_path'] != 'None':
81         path = config['pg_path']
82     try:
83         return which(name, path=path)
84     except IOError:
85         return None
86
87 def exec_pg_command(name, *args):
88     prog = find_pg_tool(name)
89     if not prog:
90         raise Exception('Couldn\'t find %s' % name)
91     args2 = (prog,) + args
92
93     with open(os.devnull) as dn:
94         return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
95
96 def exec_pg_command_pipe(name, *args):
97     prog = find_pg_tool(name)
98     if not prog:
99         raise Exception('Couldn\'t find %s' % name)
100     # on win32, passing close_fds=True is not compatible
101     # with redirecting std[in/err/out]
102     pop = subprocess.Popen((prog,) + args, bufsize= -1,
103           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
104           close_fds=(os.name=="posix"))
105     return pop.stdin, pop.stdout
106
107 def exec_command_pipe(name, *args):
108     prog = find_in_path(name)
109     if not prog:
110         raise Exception('Couldn\'t find %s' % name)
111     # on win32, passing close_fds=True is not compatible
112     # with redirecting std[in/err/out]
113     pop = subprocess.Popen((prog,) + args, bufsize= -1,
114           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
115           close_fds=(os.name=="posix"))
116     return pop.stdin, pop.stdout
117
118 #----------------------------------------------------------
119 # File paths
120 #----------------------------------------------------------
121 #file_path_root = os.getcwd()
122 #file_path_addons = os.path.join(file_path_root, 'addons')
123
124 def file_open(name, mode="r", subdir='addons', pathinfo=False):
125     """Open a file from the OpenERP root, using a subdir folder.
126
127     Example::
128     
129     >>> file_open('hr/report/timesheer.xsl')
130     >>> file_open('addons/hr/report/timesheet.xsl')
131     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
132
133     @param name name of the file
134     @param mode file open mode
135     @param subdir subdirectory
136     @param pathinfo if True returns tuple (fileobject, filepath)
137
138     @return fileobject if pathinfo is False else (fileobject, filepath)
139     """
140     import openerp.modules as addons
141     adps = addons.module.ad_paths
142     rtp = os.path.normcase(os.path.abspath(config['root_path']))
143
144     basename = name
145
146     if os.path.isabs(name):
147         # It is an absolute path
148         # Is it below 'addons_path' or 'root_path'?
149         name = os.path.normcase(os.path.normpath(name))
150         for root in adps + [rtp]:
151             root = os.path.normcase(os.path.normpath(root)) + os.sep
152             if name.startswith(root):
153                 base = root.rstrip(os.sep)
154                 name = name[len(base) + 1:]
155                 break
156         else:
157             # It is outside the OpenERP root: skip zipfile lookup.
158             base, name = os.path.split(name)
159         return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
160
161     if name.replace(os.sep, '/').startswith('addons/'):
162         subdir = 'addons'
163         name2 = name[7:]
164     elif subdir:
165         name = os.path.join(subdir, name)
166         if name.replace(os.sep, '/').startswith('addons/'):
167             subdir = 'addons'
168             name2 = name[7:]
169         else:
170             name2 = name
171
172     # First, try to locate in addons_path
173     if subdir:
174         for adp in adps:
175             try:
176                 return _fileopen(name2, mode=mode, basedir=adp,
177                                  pathinfo=pathinfo, basename=basename)
178             except IOError:
179                 pass
180
181     # Second, try to locate in root_path
182     return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
183
184
185 def _fileopen(path, mode, basedir, pathinfo, basename=None):
186     name = os.path.normpath(os.path.join(basedir, path))
187
188     if basename is None:
189         basename = name
190     # Give higher priority to module directories, which is
191     # a more common case than zipped modules.
192     if os.path.isfile(name):
193         fo = open(name, mode)
194         if pathinfo:
195             return fo, name
196         return fo
197
198     # Support for loading modules in zipped form.
199     # This will not work for zipped modules that are sitting
200     # outside of known addons paths.
201     head = os.path.normpath(path)
202     zipname = False
203     while os.sep in head:
204         head, tail = os.path.split(head)
205         if not tail:
206             break
207         if zipname:
208             zipname = os.path.join(tail, zipname)
209         else:
210             zipname = tail
211         zpath = os.path.join(basedir, head + '.zip')
212         if zipfile.is_zipfile(zpath):
213             from cStringIO import StringIO
214             zfile = zipfile.ZipFile(zpath)
215             try:
216                 fo = StringIO()
217                 fo.write(zfile.read(os.path.join(
218                     os.path.basename(head), zipname).replace(
219                         os.sep, '/')))
220                 fo.seek(0)
221                 if pathinfo:
222                     return fo, name
223                 return fo
224             except Exception:
225                 pass
226     # Not found
227     if name.endswith('.rml'):
228         raise IOError('Report %r doesn\'t exist or deleted' % basename)
229     raise IOError('File not found: %s' % basename)
230
231
232 #----------------------------------------------------------
233 # iterables
234 #----------------------------------------------------------
235 def flatten(list):
236     """Flatten a list of elements into a uniqu list
237     Author: Christophe Simonis (christophe@tinyerp.com)
238
239     Examples::
240     >>> flatten(['a'])
241     ['a']
242     >>> flatten('b')
243     ['b']
244     >>> flatten( [] )
245     []
246     >>> flatten( [[], [[]]] )
247     []
248     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
249     ['a', 'b', 'c', 'd', 'e', 'f']
250     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
251     >>> flatten(t)
252     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
253     """
254
255     def isiterable(x):
256         return hasattr(x, "__iter__")
257
258     r = []
259     for e in list:
260         if isiterable(e):
261             map(r.append, flatten(e))
262         else:
263             r.append(e)
264     return r
265
266 def reverse_enumerate(l):
267     """Like enumerate but in the other sens
268     
269     Usage::
270     >>> a = ['a', 'b', 'c']
271     >>> it = reverse_enumerate(a)
272     >>> it.next()
273     (2, 'c')
274     >>> it.next()
275     (1, 'b')
276     >>> it.next()
277     (0, 'a')
278     >>> it.next()
279     Traceback (most recent call last):
280       File "<stdin>", line 1, in <module>
281     StopIteration
282     """
283     return izip(xrange(len(l)-1, -1, -1), reversed(l))
284
285 def topological_sort(elems):
286     """ Return a list of elements sorted so that their dependencies are listed
287     before them in the result.
288
289     :param elems: specifies the elements to sort with their dependencies; it is
290         a dictionary like `{element: dependencies}` where `dependencies` is a
291         collection of elements that must appear before `element`. The elements
292         of `dependencies` are not required to appear in `elems`; they will
293         simply not appear in the result.
294
295     :returns: a list with the keys of `elems` sorted according to their
296         specification.
297     """
298     # the algorithm is inspired by [Tarjan 1976],
299     # http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
300     result = []
301     visited = set()
302
303     def visit(n):
304         if n not in visited:
305             visited.add(n)
306             if n in elems:
307                 # first visit all dependencies of n, then append n to result
308                 map(visit, elems[n])
309                 result.append(n)
310
311     map(visit, elems)
312
313     return result
314
315
316 class UpdateableStr(local):
317     """ Class that stores an updateable string (used in wizards)
318     """
319
320     def __init__(self, string=''):
321         self.string = string
322
323     def __str__(self):
324         return str(self.string)
325
326     def __repr__(self):
327         return str(self.string)
328
329     def __nonzero__(self):
330         return bool(self.string)
331
332
333 class UpdateableDict(local):
334     """Stores an updateable dict to use in wizards
335     """
336
337     def __init__(self, dict=None):
338         if dict is None:
339             dict = {}
340         self.dict = dict
341
342     def __str__(self):
343         return str(self.dict)
344
345     def __repr__(self):
346         return str(self.dict)
347
348     def clear(self):
349         return self.dict.clear()
350
351     def keys(self):
352         return self.dict.keys()
353
354     def __setitem__(self, i, y):
355         self.dict.__setitem__(i, y)
356
357     def __getitem__(self, i):
358         return self.dict.__getitem__(i)
359
360     def copy(self):
361         return self.dict.copy()
362
363     def iteritems(self):
364         return self.dict.iteritems()
365
366     def iterkeys(self):
367         return self.dict.iterkeys()
368
369     def itervalues(self):
370         return self.dict.itervalues()
371
372     def pop(self, k, d=None):
373         return self.dict.pop(k, d)
374
375     def popitem(self):
376         return self.dict.popitem()
377
378     def setdefault(self, k, d=None):
379         return self.dict.setdefault(k, d)
380
381     def update(self, E, **F):
382         return self.dict.update(E, F)
383
384     def values(self):
385         return self.dict.values()
386
387     def get(self, k, d=None):
388         return self.dict.get(k, d)
389
390     def has_key(self, k):
391         return self.dict.has_key(k)
392
393     def items(self):
394         return self.dict.items()
395
396     def __cmp__(self, y):
397         return self.dict.__cmp__(y)
398
399     def __contains__(self, k):
400         return self.dict.__contains__(k)
401
402     def __delitem__(self, y):
403         return self.dict.__delitem__(y)
404
405     def __eq__(self, y):
406         return self.dict.__eq__(y)
407
408     def __ge__(self, y):
409         return self.dict.__ge__(y)
410
411     def __gt__(self, y):
412         return self.dict.__gt__(y)
413
414     def __hash__(self):
415         return self.dict.__hash__()
416
417     def __iter__(self):
418         return self.dict.__iter__()
419
420     def __le__(self, y):
421         return self.dict.__le__(y)
422
423     def __len__(self):
424         return self.dict.__len__()
425
426     def __lt__(self, y):
427         return self.dict.__lt__(y)
428
429     def __ne__(self, y):
430         return self.dict.__ne__(y)
431
432 class currency(float):
433     """ Deprecate
434     
435     .. warning::
436     
437     Don't use ! Use res.currency.round()
438     """
439
440     def __init__(self, value, accuracy=2, rounding=None):
441         if rounding is None:
442             rounding=10**-accuracy
443         self.rounding=rounding
444         self.accuracy=accuracy
445
446     def __new__(cls, value, accuracy=2, rounding=None):
447         return float.__new__(cls, round(value, accuracy))
448
449     #def __str__(self):
450     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
451     #   return str(display_value)
452
453 def to_xml(s):
454     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
455
456 def get_iso_codes(lang):
457     if lang.find('_') != -1:
458         if lang.split('_')[0] == lang.split('_')[1].lower():
459             lang = lang.split('_')[0]
460     return lang
461
462 ALL_LANGUAGES = {
463         'ab_RU': u'Abkhazian / аҧсуа',
464         'am_ET': u'Amharic / አምሃርኛ',
465         'ar_SY': u'Arabic / الْعَرَبيّة',
466         'bg_BG': u'Bulgarian / български език',
467         'bs_BA': u'Bosnian / bosanski jezik',
468         'ca_ES': u'Catalan / Català',
469         'cs_CZ': u'Czech / Čeština',
470         'da_DK': u'Danish / Dansk',
471         'de_DE': u'German / Deutsch',
472         'el_GR': u'Greek / Ελληνικά',
473         'en_CA': u'English (CA)',
474         'en_GB': u'English (UK)',
475         'en_US': u'English (US)',
476         'es_AR': u'Spanish (AR) / Español (AR)',
477         'es_BO': u'Spanish (BO) / Español (BO)',
478         'es_CL': u'Spanish (CL) / Español (CL)',
479         'es_CO': u'Spanish (CO) / Español (CO)',
480         'es_CR': u'Spanish (CR) / Español (CR)',
481         'es_DO': u'Spanish (DO) / Español (DO)',
482         'es_EC': u'Spanish (EC) / Español (EC)',
483         'es_ES': u'Spanish / Español',
484         'es_GT': u'Spanish (GT) / Español (GT)',
485         'es_HN': u'Spanish (HN) / Español (HN)',
486         'es_MX': u'Spanish (MX) / Español (MX)',
487         'es_NI': u'Spanish (NI) / Español (NI)',
488         'es_PA': u'Spanish (PA) / Español (PA)',
489         'es_PE': u'Spanish (PE) / Español (PE)',
490         'es_PR': u'Spanish (PR) / Español (PR)',
491         'es_PY': u'Spanish (PY) / Español (PY)',
492         'es_SV': u'Spanish (SV) / Español (SV)',
493         'es_UY': u'Spanish (UY) / Español (UY)',
494         'es_VE': u'Spanish (VE) / Español (VE)',
495         'et_EE': u'Estonian / Eesti keel',
496         'fa_IR': u'Persian / فارس',
497         'fi_FI': u'Finnish / Suomi',
498         'fr_BE': u'French (BE) / Français (BE)',
499         'fr_CA': u'French (CA) / Français (CA)',
500         'fr_CH': u'French (CH) / Français (CH)',
501         'fr_FR': u'French / Français',
502         'gl_ES': u'Galician / Galego',
503         'gu_IN': u'Gujarati / ગુજરાતી',
504         'he_IL': u'Hebrew / עִבְרִי',
505         'hi_IN': u'Hindi / हिंदी',
506         'hr_HR': u'Croatian / hrvatski jezik',
507         'hu_HU': u'Hungarian / Magyar',
508         'id_ID': u'Indonesian / Bahasa Indonesia',
509         'it_IT': u'Italian / Italiano',
510         'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
511         'ja_JP': u'Japanese / 日本語',
512         'ko_KP': u'Korean (KP) / 한국어 (KP)',
513         'ko_KR': u'Korean (KR) / 한국어 (KR)',
514         'lo_LA': u'Lao / ພາສາລາວ',
515         'lt_LT': u'Lithuanian / Lietuvių kalba',
516         'lv_LV': u'Latvian / latviešu valoda',
517         'mk_MK': u'Macedonian / македонски јазик',
518         'ml_IN': u'Malayalam / മലയാളം',
519         'mn_MN': u'Mongolian / монгол',
520         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
521         'nl_NL': u'Dutch / Nederlands',
522         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
523         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
524         'pl_PL': u'Polish / Język polski',
525         'pt_BR': u'Portuguese (BR) / Português (BR)',
526         'pt_PT': u'Portuguese / Português',
527         'ro_RO': u'Romanian / română',
528         'ru_RU': u'Russian / русский язык',
529         'si_LK': u'Sinhalese / සිංහල',
530         'sl_SI': u'Slovenian / slovenščina',
531         'sk_SK': u'Slovak / Slovenský jazyk',
532         'sq_AL': u'Albanian / Shqip',
533         'sr_RS': u'Serbian (Cyrillic) / српски',
534         'sr@latin': u'Serbian (Latin) / srpski',
535         'sv_SE': u'Swedish / svenska',
536         'te_IN': u'Telugu / తెలుగు',
537         'tr_TR': u'Turkish / Türkçe',
538         'vi_VN': u'Vietnamese / Tiếng Việt',
539         'uk_UA': u'Ukrainian / українська',
540         'ur_PK': u'Urdu / اردو',
541         'zh_CN': u'Chinese (CN) / 简体中文',
542         'zh_HK': u'Chinese (HK)',
543         'zh_TW': u'Chinese (TW) / 正體字',
544         'th_TH': u'Thai / ภาษาไทย',
545         'tlh_TLH': u'Klingon',
546     }
547
548 def scan_languages():
549     """ Returns all languages supported by OpenERP for translation
550
551     :returns: a list of (lang_code, lang_name) pairs
552     :rtype: [(str, unicode)]
553     """
554     return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
555
556 def get_user_companies(cr, user):
557     def _get_company_children(cr, ids):
558         if not ids:
559             return []
560         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
561         res = [x[0] for x in cr.fetchall()]
562         res.extend(_get_company_children(cr, res))
563         return res
564     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
565     user_comp = cr.fetchone()[0]
566     if not user_comp:
567         return []
568     return [user_comp] + _get_company_children(cr, [user_comp])
569
570 def mod10r(number):
571     """
572     Input number : account or invoice number
573     Output return: the same number completed with the recursive mod10
574     key
575     """
576     codec=[0,9,4,6,8,2,7,1,3,5]
577     report = 0
578     result=""
579     for digit in number:
580         result += digit
581         if digit.isdigit():
582             report = codec[ (int(digit) + report) % 10 ]
583     return result + str((10 - report) % 10)
584
585
586 def human_size(sz):
587     """
588     Return the size in a human readable format
589     """
590     if not sz:
591         return False
592     units = ('bytes', 'Kb', 'Mb', 'Gb')
593     if isinstance(sz,basestring):
594         sz=len(sz)
595     s, i = float(sz), 0
596     while s >= 1024 and i < len(units)-1:
597         s /= 1024
598         i += 1
599     return "%0.2f %s" % (s, units[i])
600
601 def logged(f):
602     @wraps(f)
603     def wrapper(*args, **kwargs):
604         from pprint import pformat
605
606         vector = ['Call -> function: %r' % f]
607         for i, arg in enumerate(args):
608             vector.append('  arg %02d: %s' % (i, pformat(arg)))
609         for key, value in kwargs.items():
610             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
611
612         timeb4 = time.time()
613         res = f(*args, **kwargs)
614
615         vector.append('  result: %s' % pformat(res))
616         vector.append('  time delta: %s' % (time.time() - timeb4))
617         _logger.debug('\n'.join(vector))
618         return res
619
620     return wrapper
621
622 class profile(object):
623     def __init__(self, fname=None):
624         self.fname = fname
625
626     def __call__(self, f):
627         @wraps(f)
628         def wrapper(*args, **kwargs):
629             profile = cProfile.Profile()
630             result = profile.runcall(f, *args, **kwargs)
631             profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
632             return result
633
634         return wrapper
635
636 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
637 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
638 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
639 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
640 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
641 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
642 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
643 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
644 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
645 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
646 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
647 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
648 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
649 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
650 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
651 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
652 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
653 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
654 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
655 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
656 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
657 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
658 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
659 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
660 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
661 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
662 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
663 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
664 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
665 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
666 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
667 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
668 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
669 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
670 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
671 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
672 ]
673
674 def icons(*a, **kw):
675     global __icons_list
676     return [(x, x) for x in __icons_list ]
677
678 def detect_ip_addr():
679     """Try a very crude method to figure out a valid external
680        IP or hostname for the current machine. Don't rely on this
681        for binding to an interface, but it could be used as basis
682        for constructing a remote URL to the server.
683     """
684     def _detect_ip_addr():
685         from array import array
686         from struct import pack, unpack
687
688         try:
689             import fcntl
690         except ImportError:
691             fcntl = None
692
693         ip_addr = None
694
695         if not fcntl: # not UNIX:
696             host = socket.gethostname()
697             ip_addr = socket.gethostbyname(host)
698         else: # UNIX:
699             # get all interfaces:
700             nbytes = 128 * 32
701             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
702             names = array('B', '\0' * nbytes)
703             #print 'names: ', names
704             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
705             namestr = names.tostring()
706
707             # try 64 bit kernel:
708             for i in range(0, outbytes, 40):
709                 name = namestr[i:i+16].split('\0', 1)[0]
710                 if name != 'lo':
711                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
712                     break
713
714             # try 32 bit kernel:
715             if ip_addr is None:
716                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
717
718                 for ifname in [iface for iface in ifaces if iface != 'lo']:
719                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
720                     break
721
722         return ip_addr or 'localhost'
723
724     try:
725         ip_addr = _detect_ip_addr()
726     except Exception:
727         ip_addr = 'localhost'
728     return ip_addr
729
730 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
731 #  The server side never does any timestamp calculation, always
732 #  sends them in a naive (timezone agnostic) format supposed to be
733 #  expressed within the server timezone, and expects the clients to
734 #  provide timestamps in the server timezone as well.
735 #  It stores all timestamps in the database in naive format as well,
736 #  which also expresses the time in the server timezone.
737 #  For this reason the server makes its timezone name available via the
738 #  common/timezone_get() rpc method, which clients need to read
739 #  to know the appropriate time offset to use when reading/writing
740 #  times.
741 def get_win32_timezone():
742     """Attempt to return the "standard name" of the current timezone on a win32 system.
743        @return the standard name of the current win32 timezone, or False if it cannot be found.
744     """
745     res = False
746     if sys.platform == "win32":
747         try:
748             import _winreg
749             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
750             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
751             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
752             _winreg.CloseKey(current_tz_key)
753             _winreg.CloseKey(hklm)
754         except Exception:
755             pass
756     return res
757
758 def detect_server_timezone():
759     """Attempt to detect the timezone to use on the server side.
760        Defaults to UTC if no working timezone can be found.
761        @return the timezone identifier as expected by pytz.timezone.
762     """
763     try:
764         import pytz
765     except Exception:
766         _logger.warning("Python pytz module is not available. "
767             "Timezone will be set to UTC by default.")
768         return 'UTC'
769
770     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
771     # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
772     # Option 3: the environment variable TZ
773     sources = [ (config['timezone'], 'OpenERP configuration'),
774                 (time.tzname[0], 'time.tzname'),
775                 (os.environ.get('TZ',False),'TZ environment variable'), ]
776     # Option 4: OS-specific: /etc/timezone on Unix
777     if os.path.exists("/etc/timezone"):
778         tz_value = False
779         try:
780             f = open("/etc/timezone")
781             tz_value = f.read(128).strip()
782         except Exception:
783             pass
784         finally:
785             f.close()
786         sources.append((tz_value,"/etc/timezone file"))
787     # Option 5: timezone info from registry on Win32
788     if sys.platform == "win32":
789         # Timezone info is stored in windows registry.
790         # However this is not likely to work very well as the standard name
791         # of timezones in windows is rarely something that is known to pytz.
792         # But that's ok, it is always possible to use a config option to set
793         # it explicitly.
794         sources.append((get_win32_timezone(),"Windows Registry"))
795
796     for (value,source) in sources:
797         if value:
798             try:
799                 tz = pytz.timezone(value)
800                 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
801                 return value
802             except pytz.UnknownTimeZoneError:
803                 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
804
805     _logger.warning("No valid timezone could be detected, using default UTC "
806         "timezone. You can specify it explicitly with option 'timezone' in "
807         "the server configuration.")
808     return 'UTC'
809
810 def get_server_timezone():
811     return "UTC"
812
813
814 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
815 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
816 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
817     DEFAULT_SERVER_DATE_FORMAT,
818     DEFAULT_SERVER_TIME_FORMAT)
819
820 # Python's strftime supports only the format directives
821 # that are available on the platform's libc, so in order to
822 # be cross-platform we map to the directives required by
823 # the C standard (1989 version), always available on platforms
824 # with a C standard implementation.
825 DATETIME_FORMATS_MAP = {
826         '%C': '', # century
827         '%D': '%m/%d/%Y', # modified %y->%Y
828         '%e': '%d',
829         '%E': '', # special modifier
830         '%F': '%Y-%m-%d',
831         '%g': '%Y', # modified %y->%Y
832         '%G': '%Y',
833         '%h': '%b',
834         '%k': '%H',
835         '%l': '%I',
836         '%n': '\n',
837         '%O': '', # special modifier
838         '%P': '%p',
839         '%R': '%H:%M',
840         '%r': '%I:%M:%S %p',
841         '%s': '', #num of seconds since epoch
842         '%T': '%H:%M:%S',
843         '%t': ' ', # tab
844         '%u': ' %w',
845         '%V': '%W',
846         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
847         '%+': '%Y-%m-%d %H:%M:%S',
848
849         # %Z is a special case that causes 2 problems at least:
850         #  - the timezone names we use (in res_user.context_tz) come
851         #    from pytz, but not all these names are recognized by
852         #    strptime(), so we cannot convert in both directions
853         #    when such a timezone is selected and %Z is in the format
854         #  - %Z is replaced by an empty string in strftime() when
855         #    there is not tzinfo in a datetime value (e.g when the user
856         #    did not pick a context_tz). The resulting string does not
857         #    parse back if the format requires %Z.
858         # As a consequence, we strip it completely from format strings.
859         # The user can always have a look at the context_tz in
860         # preferences to check the timezone.
861         '%z': '',
862         '%Z': '',
863 }
864
865 POSIX_TO_LDML = {
866     'a': 'E',
867     'A': 'EEEE',
868     'b': 'MMM',
869     'B': 'MMMM',
870     #'c': '',
871     'd': 'dd',
872     'H': 'HH',
873     'I': 'hh',
874     'j': 'DDD',
875     'm': 'MM',
876     'M': 'mm',
877     'p': 'a',
878     'S': 'ss',
879     'U': 'w',
880     'w': 'e',
881     'W': 'w',
882     'y': 'yy',
883     'Y': 'yyyy',
884     # see comments above, and babel's format_datetime assumes an UTC timezone
885     # for naive datetime objects
886     #'z': 'Z',
887     #'Z': 'z',
888 }
889
890 def posix_to_ldml(fmt, locale):
891     """ Converts a posix/strftime pattern into an LDML date format pattern.
892
893     :param fmt: non-extended C89/C90 strftime pattern
894     :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
895     :return: unicode
896     """
897     buf = []
898     pc = False
899     quoted = []
900
901     for c in fmt:
902         # LDML date format patterns uses letters, so letters must be quoted
903         if not pc and c.isalpha():
904             quoted.append(c if c != "'" else "''")
905             continue
906         if quoted:
907             buf.append("'")
908             buf.append(''.join(quoted))
909             buf.append("'")
910             quoted = []
911
912         if pc:
913             if c == '%': # escaped percent
914                 buf.append('%')
915             elif c == 'x': # date format, short seems to match
916                 buf.append(locale.date_formats['short'].pattern)
917             elif c == 'X': # time format, seems to include seconds. short does not
918                 buf.append(locale.time_formats['medium'].pattern)
919             else: # look up format char in static mapping
920                 buf.append(POSIX_TO_LDML[c])
921             pc = False
922         elif c == '%':
923             pc = True
924         else:
925             buf.append(c)
926
927     # flush anything remaining in quoted buffer
928     if quoted:
929         buf.append("'")
930         buf.append(''.join(quoted))
931         buf.append("'")
932
933     return ''.join(buf)
934
935 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
936         tz_offset=True, ignore_unparsable_time=True):
937     """
938     Convert a source timestamp string into a destination timestamp string, attempting to apply the
939     correct offset if both the server and local timezone are recognized, or no
940     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
941
942     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
943              the client would not be able to format/offset it correctly. DO NOT use it for returning
944              date fields directly, these are supposed to be handled by the client!!
945
946     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
947     @param src_format: the format to use when parsing the server timestamp.
948     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
949     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
950     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
951                                    using src_format or formatted using dst_format.
952
953     @return local/client formatted timestamp, expressed in the local/client timezone if possible
954             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
955     """
956     if not src_tstamp_str:
957         return False
958
959     res = src_tstamp_str
960     if src_format and dst_format:
961         # find out server timezone
962         server_tz = get_server_timezone()
963         try:
964             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
965             dt_value = datetime.strptime(src_tstamp_str, src_format)
966             if tz_offset and dst_tz_name:
967                 try:
968                     import pytz
969                     src_tz = pytz.timezone(server_tz)
970                     dst_tz = pytz.timezone(dst_tz_name)
971                     src_dt = src_tz.localize(dt_value, is_dst=True)
972                     dt_value = src_dt.astimezone(dst_tz)
973                 except Exception:
974                     pass
975             res = dt_value.strftime(dst_format)
976         except Exception:
977             # Normal ways to end up here are if strptime or strftime failed
978             if not ignore_unparsable_time:
979                 return False
980     return res
981
982
983 def split_every(n, iterable, piece_maker=tuple):
984     """Splits an iterable into length-n pieces. The last piece will be shorter
985        if ``n`` does not evenly divide the iterable length.
986        @param ``piece_maker``: function to build the pieces
987        from the slices (tuple,list,...)
988     """
989     iterator = iter(iterable)
990     piece = piece_maker(islice(iterator, n))
991     while piece:
992         yield piece
993         piece = piece_maker(islice(iterator, n))
994
995 if __name__ == '__main__':
996     import doctest
997     doctest.testmod()
998
999 class upload_data_thread(threading.Thread):
1000     def __init__(self, email, data, type):
1001         self.args = [('email',email),('type',type),('data',data)]
1002         super(upload_data_thread,self).__init__()
1003     def run(self):
1004         try:
1005             import urllib
1006             args = urllib.urlencode(self.args)
1007             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1008             fp.read()
1009             fp.close()
1010         except Exception:
1011             pass
1012
1013 def upload_data(email, data, type='SURVEY'):
1014     a = upload_data_thread(email, data, type)
1015     a.start()
1016     return True
1017
1018 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
1019     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
1020
1021        :param string field: name of the field we want to read and group by
1022        :return: mapping of field values to the list of ids that have it
1023        :rtype: dict
1024     """
1025     res = {}
1026     for record in obj.read(cr, uid, ids, [field], context=context):
1027         key = record[field]
1028         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
1029     return res
1030
1031 def get_and_group_by_company(cr, uid, obj, ids, context=None):
1032     return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
1033
1034 # port of python 2.6's attrgetter with support for dotted notation
1035 def resolve_attr(obj, attr):
1036     for name in attr.split("."):
1037         obj = getattr(obj, name)
1038     return obj
1039
1040 def attrgetter(*items):
1041     if len(items) == 1:
1042         attr = items[0]
1043         def g(obj):
1044             return resolve_attr(obj, attr)
1045     else:
1046         def g(obj):
1047             return tuple(resolve_attr(obj, attr) for attr in items)
1048     return g
1049
1050 class unquote(str):
1051     """A subclass of str that implements repr() without enclosing quotation marks
1052        or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1053        One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1054        of a dict's repr(). Use with care.
1055
1056        Some examples (notice that there are never quotes surrounding
1057        the ``active_id`` name:
1058
1059        >>> unquote('active_id')
1060        active_id
1061        >>> d = {'test': unquote('active_id')}
1062        >>> d
1063        {'test': active_id}
1064        >>> print d
1065        {'test': active_id}
1066     """
1067     def __repr__(self):
1068         return self
1069
1070 class UnquoteEvalContext(defaultdict):
1071     """Defaultdict-based evaluation context that returns 
1072        an ``unquote`` string for any missing name used during
1073        the evaluation.
1074        Mostly useful for evaluating OpenERP domains/contexts that
1075        may refer to names that are unknown at the time of eval,
1076        so that when the context/domain is converted back to a string,
1077        the original names are preserved.
1078
1079        **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1080        ``safe_eval()`` will shadow the builtins, which may cause other
1081        failures, depending on what is evaluated.
1082
1083        Example (notice that ``section_id`` is preserved in the final
1084        result) :
1085
1086        >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1087        >>> eval(context_str, UnquoteEvalContext(uid=1))
1088        {'default_user_id': 1, 'default_section_id': section_id}
1089
1090        """
1091     def __init__(self, *args, **kwargs):
1092         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1093
1094     def __missing__(self, key):
1095         return unquote(key)
1096
1097
1098 class mute_logger(object):
1099     """Temporary suppress the logging.
1100     Can be used as context manager or decorator.
1101
1102         @mute_logger('openerp.plic.ploc')
1103         def do_stuff():
1104             blahblah()
1105
1106         with mute_logger('openerp.foo.bar'):
1107             do_suff()
1108
1109     """
1110     def __init__(self, *loggers):
1111         self.loggers = loggers
1112
1113     def filter(self, record):
1114         return 0
1115
1116     def __enter__(self):
1117         for logger in self.loggers:
1118             assert isinstance(logger, basestring),\
1119                 "A logger name must be a string, got %s" % type(logger)
1120             logging.getLogger(logger).addFilter(self)
1121
1122     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1123         for logger in self.loggers:
1124             logging.getLogger(logger).removeFilter(self)
1125
1126     def __call__(self, func):
1127         @wraps(func)
1128         def deco(*args, **kwargs):
1129             with self:
1130                 return func(*args, **kwargs)
1131         return deco
1132
1133 _ph = object()
1134 class CountingStream(object):
1135     """ Stream wrapper counting the number of element it has yielded. Similar
1136     role to ``enumerate``, but for use when the iteration process of the stream
1137     isn't fully under caller control (the stream can be iterated from multiple
1138     points including within a library)
1139
1140     ``start`` allows overriding the starting index (the index before the first
1141     item is returned).
1142
1143     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1144     by one.
1145
1146     .. attribute:: index
1147
1148         ``int``, index of the last yielded element in the stream. If the stream
1149         has ended, will give an index 1-past the stream
1150     """
1151     def __init__(self, stream, start=-1):
1152         self.stream = iter(stream)
1153         self.index = start
1154         self.stopped = False
1155     def __iter__(self):
1156         return self
1157     def next(self):
1158         if self.stopped: raise StopIteration()
1159         self.index += 1
1160         val = next(self.stream, _ph)
1161         if val is _ph:
1162             self.stopped = True
1163             raise StopIteration()
1164         return val
1165
1166 def stripped_sys_argv(*strip_args):
1167     """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1168     strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
1169     assert all(config.parser.has_option(s) for s in strip_args)
1170     takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1171
1172     longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1173     longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1174
1175     args = sys.argv[:]
1176
1177     def strip(args, i):
1178         return args[i].startswith(shorts) \
1179             or args[i].startswith(longs_eq) or (args[i] in longs) \
1180             or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1181
1182     return [x for i, x in enumerate(args) if not strip(args, i)]
1183
1184
1185 class ConstantMapping(Mapping):
1186     """
1187     An immutable mapping returning the provided value for every single key.
1188
1189     Useful for default value to methods
1190     """
1191     __slots__ = ['_value']
1192     def __init__(self, val):
1193         self._value = val
1194
1195     def __len__(self):
1196         """
1197         defaultdict updates its length for each individually requested key, is
1198         that really useful?
1199         """
1200         return 0
1201
1202     def __iter__(self):
1203         """
1204         same as len, defaultdict udpates its iterable keyset with each key
1205         requested, is there a point for this?
1206         """
1207         return iter([])
1208
1209     def __getitem__(self, item):
1210         return self._value
1211
1212
1213 def dumpstacks(sig=None, frame=None):
1214     """ Signal handler: dump a stack trace for each existing thread."""
1215     code = []
1216
1217     def extract_stack(stack):
1218         for filename, lineno, name, line in traceback.extract_stack(stack):
1219             yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
1220             if line:
1221                 yield "  %s" % (line.strip(),)
1222
1223     # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
1224     # modified for python 2.5 compatibility
1225     threads_info = dict([(th.ident, {'name': th.name, 'uid': getattr(th, 'uid', 'n/a')})
1226                         for th in threading.enumerate()])
1227     for threadId, stack in sys._current_frames().items():
1228         thread_info = threads_info.get(threadId)
1229         code.append("\n# Thread: %s (id:%s) (uid:%s)" %
1230                     (thread_info and thread_info['name'] or 'n/a',
1231                      threadId,
1232                      thread_info and thread_info['uid'] or 'n/a'))
1233         for line in extract_stack(stack):
1234             code.append(line)
1235
1236     if openerp.evented:
1237         # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1238         import gc
1239         from greenlet import greenlet
1240         for ob in gc.get_objects():
1241             if not isinstance(ob, greenlet) or not ob:
1242                 continue
1243             code.append("\n# Greenlet: %r" % (ob,))
1244             for line in extract_stack(ob.gr_frame):
1245                 code.append(line)
1246
1247     _logger.info("\n".join(code))
1248
1249 class frozendict(dict):
1250     """ An implementation of an immutable dictionary. """
1251     def __delitem__(self, key):
1252         raise NotImplementedError("'__delitem__' not supported on frozendict")
1253     def __setitem__(self, key, val):
1254         raise NotImplementedError("'__setitem__' not supported on frozendict")
1255     def clear(self):
1256         raise NotImplementedError("'clear' not supported on frozendict")
1257     def pop(self, key, default=None):
1258         raise NotImplementedError("'pop' not supported on frozendict")
1259     def popitem(self):
1260         raise NotImplementedError("'popitem' not supported on frozendict")
1261     def setdefault(self, key, default=None):
1262         raise NotImplementedError("'setdefault' not supported on frozendict")
1263     def update(self, *args, **kwargs):
1264         raise NotImplementedError("'update' not supported on frozendict")
1265
1266 @contextmanager
1267 def ignore(*exc):
1268     try:
1269         yield
1270     except exc:
1271         pass
1272
1273 # Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
1274 if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
1275     def html_escape(text):
1276         return werkzeug.utils.escape(text, quote=True)
1277 else:
1278     def html_escape(text):
1279         return werkzeug.utils.escape(text)
1280
1281 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: