source: main/waeup.kofa/trunk/src/waeup/kofa/utils/helpers.py @ 7942

Last change on this file since 7942 was 7941, checked in by uli, 13 years ago

Add new helper function.

  • Property svn:keywords set to Id
File size: 15.8 KB
Line 
1## $Id: helpers.py 7941 2012-03-22 01:48:58Z uli $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
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## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions for Kofa.
19"""
20import os
21import re
22import sys
23import shutil
24import grok
25from cStringIO import StringIO
26from docutils.core import publish_string
27from zope.component import getUtility, getUtilitiesFor
28from zope.component.interfaces import IFactory
29from zope.interface import implementedBy
30from zope.interface.interface import Method, Attribute
31from zope.schema import getFieldNames
32from zope.schema.fieldproperty import FieldProperty
33from zope.security.interfaces import NoInteraction
34from zope.security.management import getInteraction
35from zope.pluggableauth.interfaces import IAuthenticatorPlugin
36
37BUFSIZE = 8 * 1024
38
39def remove_file_or_directory(filepath):
40    """Remove a file or directory.
41
42    Different to :func:`shutil.rmtree` we also accept not existing
43    paths (returning silently) and if a dir turns out to be a regular
44    file, we remove that.
45    """
46    filepath = os.path.abspath(filepath)
47    if not os.path.exists(filepath):
48        return
49    if os.path.isdir(filepath):
50        shutil.rmtree(filepath)
51    else:
52        os.unlink(filepath)
53    return
54
55def copy_filesystem_tree(src, dst, overwrite=False, del_old=False):
56    """Copy contents of directory src to directory dst.
57
58    Both directories must exists.
59
60    If `overwrite` is true, any same named objects will be
61    overwritten. Otherwise these files will not be touched.
62
63    If `del_old` is true, copied files and directories will be removed
64    from the src directory.
65
66    This functions returns a list of non-copied files.
67
68    Unix hidden files and directories (starting with '.') are not
69    processed by this function.
70    """
71    if not os.path.exists(src):
72        raise ValueError('source path does not exist: %s' % src)
73    if not os.path.exists(dst):
74        raise ValueError('destination path does not exist: %s' % dst)
75    if not os.path.isdir(src):
76        raise ValueError('source path is not a directory: %s' % src)
77    if not os.path.isdir(dst):
78        raise ValueError('destination path is not a directory: %s' % dst)
79    not_copied = []
80    for item in os.listdir(src):
81        if item.startswith('.'):
82            continue # We do not copy hidden stuff...
83        itemsrc = os.path.join(src, item)
84        itemdst = os.path.join(dst, item)
85
86        if os.path.exists(itemdst):
87            if overwrite is True:
88                remove_file_or_directory(itemdst)
89            else:
90                not_copied.append(item)
91                continue
92
93        if os.path.isdir(itemsrc):
94            shutil.copytree(itemsrc, itemdst)
95        else:
96            shutil.copy2(itemsrc, itemdst)
97        if del_old:
98            remove_file_or_directory(itemsrc)
99    return not_copied
100
101
102def get_inner_HTML_part(html_code):
103    """Return the 'inner' part of a complete HTML snippet.
104
105    If there is a form part, get this.
106
107    If there is no form part, try to return the body part contents.
108
109    If there is no body, return as-is.
110
111    Let's see how that works. If we deliver some doc with form, we
112    will get that form only:
113
114       >>> doc = '<html><form>My Form</form>Outside the form</html>'
115       >>> get_inner_HTML_part(doc)
116       '<form>My Form</form>'
117
118    No form? Then seek for a body part and get the contents:
119
120       >>> doc = '<html><body>My Body</body>Trailing Trash</html>'
121       >>> get_inner_HTML_part(doc)
122       'My Body'
123
124    If none of these is included, return what we got:
125
126       >>> doc = '<html>without body nor form</html>'
127       >>> get_inner_HTML_part(doc)
128       '<html>without body nor form</html>'
129
130    """
131
132    try:
133        result = re.match('^.+(<form[^\>]*>.*</form>).+$', html_code,
134                          re.DOTALL).groups()[0]
135        return result
136    except AttributeError:
137        # No <form> part included
138        try:
139            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
140                              re.DOTALL).groups()[0]
141            return result
142        except AttributeError:
143            # No <form> and no <body> tag...
144            pass
145    return html_code
146
147class FactoryBase(grok.GlobalUtility):
148    """A factory for things.
149
150    This is a baseclass for easier creation of factories. Factories
151    are utilities that are registered under a certain name and return
152    instances of certain classes when called.
153
154    In :mod:`waeup.kofa` we use factories extensively for
155    batching. While processing a batch some processors looks up a
156    factory to create real-world instances that then get filled with
157    data from imported CSV files.
158
159    To get rid of reimplementing the same stuff over and over again,
160    most notably the methods defined here, we offer this base class
161    (which will *not* be registered as a factory itself).
162
163    Real factories can then be created like this:
164
165       >>> import grok
166       >>> from waeup.kofa.utils.helpers import FactoryBase
167       >>> class MyObject(object):
168       ...   # Some class we want to get instances of.
169       ...   pass
170       >>> class MyObjectFactory(FactoryBase):
171       ...   # This is the factory for MyObject instances
172       ...   grok.name(u'waeup.kofa.factory.MyObject')
173       ...   factory = MyObject
174
175    That's it. It is essential to set the ``factory`` attribute, which
176    will determine the class of which instances should be created when
177    called. The given name must even be unique amongst all utilities
178    registered during runtime. While you can pick any name you like
179    you might want to prepend ``waeup.kofa.factory.`` to the name
180    string to make sure it does not clash with names of other
181    utilities one day.
182
183    Before all this works we have to grok the baseclass once and our
184    freshly defined factory. This executes all the component
185    registration stuff we don't want to do ourselves. In daily use
186    this is done automatically on startup of a :mod:`waeup.kofa`
187    system.
188
189       >>> grok.testing.grok('waeup.kofa.utils.helpers')
190       >>> grok.testing.grok_component(
191       ...    'MyObjectFactory', MyObjectFactory
192       ...  )
193       True
194
195    After grokking we (and processors) can create objects without
196    knowing about the location of the real class definition, just by
197    the factory name:
198
199       >>> from zope.component import createObject
200       >>> obj = createObject('waeup.kofa.factory.MyObject')
201       >>> isinstance(obj, MyObject)
202       True
203
204    We can also use the regular utility lookups to find our new
205    factory:
206
207       >>> from zope.component import getUtility
208       >>> from zope.component.interfaces import IFactory
209       >>> factory = getUtility(
210       ...   IFactory, name='waeup.kofa.factory.MyObject'
211       ...   )
212       >>> isinstance(factory, MyObjectFactory)
213       True
214
215    And this factory generates `MyObject` instances:
216
217       >>> obj = factory()
218       >>> isinstance(obj, MyObject)
219       True
220
221    """
222    grok.baseclass() # Do not grok this class, do not register us.
223    grok.implements(IFactory)
224    # You can override any of the following attributes in derived
225    # classes. The `grok.name` setting *must* even be set to some
226    # unique value.
227    grok.name(u'waeup.Factory')
228    title = u"Create instances of ``factory``.",
229    description = u"This factory instantiates new applicant instances."
230    factory = None
231
232    def __call__(self, *args, **kw):
233        """The main factory function.
234
235        Returns an instance of the requested object.
236        """
237        return self.factory()
238
239    def getInterfaces(self):
240        # Required by IFactory
241        return implementedBy(self.factory)
242
243def ReST2HTML_w_warnings(source_string):
244    """Convert a reStructuredText string to HTML preserving warnings.
245
246    Returns a tuple ``(<HTML_CODE>, <WARNINGS>)``, both being
247    strings. Where ``<HTML_CODE>`` is the HTML code generated from the
248    source string (in unicode), ``<WARNINGS>`` is a string containing
249    any warning messages or ``None``.
250
251    Regular multi-line ReStructuredText strings will be returned as
252    HTML code:
253
254        >>> from waeup.kofa.utils.helpers import ReST2HTML
255        >>> source = '''
256        ... Headline
257        ... ========
258        ...
259        ... - A list item
260        ... - Another item
261        ...
262        ... Thanks for watching!
263        ... '''
264        >>> html, warnings = ReST2HTML_w_warnings(source)
265        >>> print html
266        <div class="document" id="headline">
267        <h1 class="title">Headline</h1>
268        <BLANKLINE>
269        <ul class="simple">
270        <li>A list item</li>
271        <li>Another item</li>
272        </ul>
273        <p>Thanks for watching!</p>
274        </div>
275
276    Here no warnings happened, so the `warnings` are ``None``:
277
278        >>> warnings is None
279        True
280
281    If warnings happen then they can be retrieved in the returned
282    ``warnings``. We try to render an erraneous document:
283
284        >>> source = '''
285        ... Headline
286        ... ======
287        ...
288        ... Thanks for watching!
289        ... '''
290        >>> html, warnings = ReST2HTML_w_warnings(source)
291        >>> print html
292        <div class="document" id="headline">
293        <h1 class="title">Headline</h1>
294        <BLANKLINE>
295        <p>Thanks for watching!</p>
296        </div>
297
298        >>> print warnings
299        <string>:3: (WARNING/2) Title underline too short.
300        <BLANKLINE>
301        Headline
302        ======
303        <BLANKLINE>
304
305    As you can see, the warnings are not displayed inline the document
306    but can be retrieved from the returned warnings, which is a string
307    or ``None``.
308    """
309    warnings = StringIO()
310    fulldoc = publish_string(
311        source_string, writer_name='html4css1',
312        settings_overrides={
313            'report_level': 0,
314            'warning_stream': warnings,
315            })
316    warnings.seek(0)
317    warning_msgs = warnings.read()
318    if warning_msgs:
319        # Render again, this time with no warnings inline...
320        fulldoc =  publish_string(
321        source_string, writer_name='html4css1',
322        settings_overrides={
323            'report_level': 10000,
324            'halt_level': 10000,
325            'warning_stream': warnings,
326            })
327    if warning_msgs == '':
328        warning_msgs = None
329    result = get_inner_HTML_part(fulldoc).strip()
330    if not isinstance(result, unicode):
331        result = result.decode('utf-8')
332    return result, warning_msgs
333
334def ReST2HTML(source_string):
335    """Render a string containing ReStructuredText to HTML.
336
337    Any warnings about too short headings, etc. are silently
338    discarded. Use :func:`ReST2HTML_w_warnings` if you want to get any
339    warnings.
340
341    The returned string will be unicode.
342
343    A regular document will be rendered like this:
344
345        >>> source = '''
346        ... Headline
347        ... ========
348        ...
349        ... Thanks for watching!
350        ... '''
351        >>> html = ReST2HTML(source)
352        >>> print html
353        <div class="document" id="headline">
354        <h1 class="title">Headline</h1>
355        <BLANKLINE>
356        <p>Thanks for watching!</p>
357        </div>
358
359    A document with markup problems (here: the underline is too short)
360    will look similar:
361
362        >>> source = '''
363        ... Headline
364        ... ======
365        ...
366        ... Thanks for watching!
367        ... '''
368        >>> html = ReST2HTML(source)
369        >>> print html
370        <div class="document" id="headline">
371        <h1 class="title">Headline</h1>
372        <BLANKLINE>
373        <p>Thanks for watching!</p>
374        </div>
375
376    """
377    html, warnings = ReST2HTML_w_warnings(source_string)
378    return html
379
380def attrs_to_fields(cls):
381    """Turn the attributes of a class into FieldProperty instances.
382
383    With Python >= 2.6 we can even use this function as a class decorator.
384    """
385    iface = list(implementedBy(cls))[0]
386    for field_name in getFieldNames(iface):
387        setattr(cls, field_name, FieldProperty(iface[field_name]))
388    return cls
389
390def get_current_principal():
391    """Get the 'current' principal.
392
393    This method works without a request. Examining a request is the
394    regular (and recommended) way to get a principal involved
395    'currently'.
396
397    Use this method only if you really have no access to the current
398    request.
399
400    Returns ``None`` when no principal is involved (for instance
401    during tests).
402    """
403    try:
404        principal = getInteraction().participations[0].principal
405    except NoInteraction:
406        return None
407    except IndexError: # No participations present
408        return None
409    return principal
410
411def cmp_files(file_descr1, file_descr2):
412    """Compare two files by their file descriptors.
413
414    Returns ``True`` if both are equal, ``False`` otherwise.
415    """
416    file_descr1.seek(0)
417    file_descr2.seek(0)
418    while True:
419        b1 = file_descr1.read(BUFSIZE)
420        b2 = file_descr2.read(BUFSIZE)
421        if b1 != b2:
422            return False
423        if not b1:
424            return True
425
426def string_from_bytes(number):
427    """Turn a number into some textual representation.
428
429      Examples:
430
431        >>> string_from_bytes(1)
432        u'1 byte(s)'
433
434        >>> string_from_bytes(1025)
435        u'1 KB'
436
437        >>> string_from_bytes(1.5 * 1024*1024)
438        u'1.50 MB'
439
440        >>> string_from_bytes(673.286 * 1024**3)
441        u'673.29 GB'
442
443    """
444    if number < 1024:
445        return u'%s byte(s)' % (str(number),)
446    elif number < 1024**2:
447        return u'%s KB' % (number / 1024,)
448    elif number < 1024**3:
449        return u'%.2f MB' % (number / 1024**2,)
450    return u'%.2f GB' % (number / 1024**3,)
451
452def file_size(file_like_obj):
453    """Determine file size in most effective manner.
454
455    Returns the number of bytes in a file. This function works for
456    both, real files as well as file-like objects like cStringIO based
457    'files'.
458
459    Example:
460
461      >>> from cStringIO import StringIO
462      >>> file_size(StringIO('my file content'))
463      15
464
465    Please note that this function expects the file-like object passed
466    in to be at first reading position (it does no seek(0)) and that
467    when finished the file pointer might be at end of file.
468    """
469    if hasattr(file_like_obj, 'fileno'):
470        return os.fstat(file_like_obj.fileno())[6]
471    file_like_obj.seek(0, 2) # seek to last position in file
472    return file_like_obj.tell()
473
474def get_user_account(request):
475    """Return local user account.
476    """
477    principal_id = request.principal.id
478    authenticator = getUtility(IAuthenticatorPlugin, name='users')
479    account = authenticator.getAccount(principal_id)
480    return account
481
482def iface_names(iface, omit=[], exclude_attribs=True, exclude_methods=True):
483    """Get all attribute names of an interface.
484
485    Searches also base interfaces.
486
487    Names of fields that are pure attributes
488    (i.e. zope.interface.Attribute) or methods are excluded by
489    default.
490
491    Names of typical fields derived from zope.schema are included.
492
493    The `omit` paramter can give a list of names to exclude.
494
495    Returns an unsorted list of strings.
496    """
497    ifaces = [iface] + list(iface.getBases())
498    names = []
499    for item in ifaces:
500        names += item.names()
501    names = [x for x in names if x not in omit]
502    result = []
503    for name in names:
504        cls = iface.get(name).__class__
505        if exclude_attribs and cls is Attribute:
506            continue
507        if exclude_methods and cls is Method:
508            continue
509        result.append(name)
510    return result
Note: See TracBrowser for help on using the repository browser.