File: utils.py

package info (click to toggle)
kaa-base 0.6.0%2Bsvn4596-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd, stretch, wheezy
  • size: 2,348 kB
  • ctags: 3,068
  • sloc: python: 11,094; ansic: 1,862; makefile: 74
file content (694 lines) | stat: -rw-r--r-- 26,578 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# utils.py - Miscellaneous system utilities
# -----------------------------------------------------------------------------
# $Id: utils.py 4596 2011-10-22 22:22:38Z tack $
#
# -----------------------------------------------------------------------------
# Copyright 2006-2009 Dirk Meyer, Jason Tackaberry
#
# Please see the file AUTHORS for a complete list of authors.
#
# This library is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version
# 2.1 as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
#
# -----------------------------------------------------------------------------
from __future__ import absolute_import

__all__ = [
    'tempfile', 'which', 'Lock', 'daemonize', 'is_running', 'set_running',
    'set_process_name', 'get_num_cpus', 'get_machine_uuid', 'get_plugins',
    'Singleton', 'property', 'wraps', 'DecoratorDataStore', ]

import sys
import os
import stat
import time
import imp
import zipimport
import logging
import inspect
import re
import functools
import ctypes, ctypes.util
import socket
from tempfile import mktemp

from . import _utils

# get logging object
log = logging.getLogger('base')

# create tmp directory for the user
TEMP = '/tmp/kaa-%s' % os.getuid()
if os.environ.get('TMPDIR'):
    TEMP = os.path.join(os.environ['TMPDIR'], 'kaa-%s' % os.getuid())
if os.path.isdir(TEMP):
    # temp dir is already there, check permissions
    if os.path.islink(TEMP):
        raise IOError('Security Error: %s is a link, aborted' % TEMP)
    if stat.S_IMODE(os.stat(TEMP)[stat.ST_MODE]) % 01000 != 0700:
        raise IOError('Security Error: %s has wrong permissions, aborted' % TEMP)
    if os.stat(TEMP)[stat.ST_UID] != os.getuid():
        raise IOError('Security Error: %s does not belong to you, aborted' % TEMP)
else:
    os.mkdir(TEMP, 0700)


def tempfile(name, unique=False):
    """
    Return a filename in the secure kaa tmp directory with the given name.
    Name can also be a relative path in the temp directory, directories will
    be created if missing. If unique is set, it will return a unique name based
    on the given name.
    """
    name = os.path.join(TEMP, name)
    if not os.path.isdir(os.path.dirname(name)):
        os.mkdir(os.path.dirname(name))
    if not unique:
        return name
    return mktemp(prefix=os.path.basename(name), dir=os.path.dirname(name))


def which(file, path = None):
    """
    Does what which(1) does: searches the PATH for a given file
    name and returns a list of matches.
    """
    if not path:
        path = os.getenv("PATH")

    for p in path.split(":"):
        fullpath = os.path.join(p, file)
        try:
            st = os.stat(fullpath)
        except OSError:
            continue

        if os.geteuid() == st[stat.ST_UID]:
            mask = stat.S_IXUSR
        elif st[stat.ST_GID] in os.getgroups():
            mask = stat.S_IXGRP
        else:
            mask = stat.S_IXOTH

        if stat.S_IMODE(st[stat.ST_MODE]) & mask:
            return fullpath

    return None


class Lock(object):
    def __init__(self):
        self._read, self._write = os.pipe()

    def release(self, exitcode):
        os.write(self._write, str(exitcode))
        os.close(self._read)
        os.close(self._write)

    def wait(self):
        exitcode = os.read(self._read, 1)
        os.close(self._read)
        os.close(self._write)
        return int(exitcode)

    def ignore(self):
        os.close(self._read)
        os.close(self._write)


def daemonize(stdin = '/dev/null', stdout = '/dev/null', stderr = None,
              pidfile=None, exit = True, wait = False):
    """
    Does a double-fork to daemonize the current process using the technique
    described at http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 .

    If exit is True (default), parent exits immediately.  If false, caller will receive
    the pid of the forked child.
    """

    lock = 0
    if wait:
        lock = Lock()

    # First fork.
    try:
        pid = os.fork()
        if pid > 0:
            if wait:
                exitcode = lock.wait()
                if exitcode:
                    sys.exit(exitcode)
            if exit:
                # Exit from the first parent.
                sys.exit(0)

            # Wait for child to fork again (otherwise we have a zombie)
            os.waitpid(pid, 0)
            return pid
    except OSError, e:
        log.error("Initial daemonize fork failed: %d, %s\n", e.errno, e.strerror)
        sys.exit(1)

    os.chdir("/")
    os.setsid()

    # Second fork.
    try:
        pid = os.fork()
        if pid > 0:
            # Exit from the second parent.
            sys.exit(0)
    except OSError, e:
        log.error("Second daemonize fork failed: %d, %s\n", e.errno, e.strerror)
        sys.exit(1)

    # Create new standard file descriptors.
    if not stderr:
        stderr = stdout
    stdin = open(stdin, 'r')
    stdout = open(stdout, 'a+')
    stderr = open(stderr, 'a+', 0)
    if pidfile:
        open(pidfile, 'w+').write("%d\n" % os.getpid())

    # Remap standard fds.
    os.dup2(stdin.fileno(), sys.stdin.fileno())
    os.dup2(stdout.fileno(), sys.stdout.fileno())
    os.dup2(stderr.fileno(), sys.stderr.fileno())

    # Replace any existing thread notifier pipe, otherwise we'll be listening
    # to our parent's thread pipe.
    from . import main
    if main.is_initialized():
        main.init(reset=True)
    return lock


def fork():
    """
    Forks the process.  May safely be called after the main loop has been
    started.
    """
    pid = os.fork()
    if not pid:
        # Child must replace thread notifier pipe, otherwise we'll be listening
        # to our parent's thread pipe.
        from . import main
        if main.is_initialized():
            main.init(reset=True)
    return pid


def is_running(name):
    """
    Check if the program with the given name is running. The program
    must have called set_running itself. Returns the pid or 0.
    """
    if not os.path.isfile(tempfile('run/' + name)):
        return 0
    run = open(tempfile('run/' + name))
    pid = run.readline().strip()
    cmdline = run.readline()
    run.close()
    if not os.path.exists('/proc/%s/cmdline' % pid):
        return 0
    current = open('/proc/%s/cmdline' % pid).readline()
    if current == cmdline or current.strip('\x00') == name:
        return int(pid)
    return 0


def set_running(name, modify = True):
    """
    Set this program as running with the given name.  If modify is True,
    the process name is updated as described in set_process_name().
    """
    cmdline = open('/proc/%s/cmdline' % os.getpid()).readline()
    run = open(tempfile('run/' + name), 'w')
    run.write(str(os.getpid()) + '\n')
    run.write(cmdline)
    run.close()
    if modify:
        _utils.set_process_name(name, len(cmdline))


def set_process_name(name):
    """
    On Linux systems later than 2.6.9, this function sets the process name as it
    appears in ps, and so that it can be found with killall.

    Note: name will be truncated to the cumulative length of the original
    process name and all its arguments; once updated, passed arguments will no
    longer be visible.
    """
    cmdline = open('/proc/%s/cmdline' % os.getpid()).readline()
    _utils.set_process_name(name, len(cmdline))


def get_num_cpus():
    """
    Returns the number of processors on the system, or raises RuntimeError
    if that value cannot be determined.
    """
    try:
        if sys.platform == 'win32':
            return int(os.environ['NUMBER_OF_PROCESSORS'])
        elif sys.platform == 'darwin':
            return int(os.popen('sysctl -n hw.ncpu').read())
        else:
            return os.sysconf('SC_NPROCESSORS_ONLN')
    except (KeyError, ValueError, OSError, AttributeError):
        pass

    raise RuntimeError('Could not determine number of processors')


def get_machine_uuid():
    """
    Returns a unique (and hopefully persistent) identifier for the current
    machine.

    This function will return the D-Bus UUID if it exists (which should be
    available on modern Linuxes), otherwise it will return the machine's
    hostname.
    """
    # First try libdbus.
    try:
        lib = ctypes.CDLL(ctypes.util.find_library('dbus-1'))
        ptr = lib.dbus_get_local_machine_id()
        uuid = ctypes.c_char_p(ptr).value
        lib.dbus_free(ptr)
        return uuid
    except AttributeError:
        pass

    # Next try to read from filesystem at well known locations.
    for dir in '/var/lib/dbus', '/etc/dbus-1':
        try:
            return open(os.path.join(dir, 'machine-id')).readline().strip()
        except IOError:
            pass

    # No dbus, fallback to hostname.
    return socket.getfqdn()


def get_plugins(group=None, location=None, attr=None, filter=None, scope=None):
    """
    Flexible plugin loader, supporting Python eggs as well as on-disk trees.
    All modules at the specified location or entry point group (for eggs) are
    loaded and returned as a dict mapping plugin names to plugin objects.

    :param group: a setuptools entry point group name (more below)
    :type group: str or None
    :param location: path within which to load plugins.  If a filename is
                     included, it will be stripped, which means you can
                     conveniently pass ``__file__`` here.  Paths inside eggs are
                     also supported.  All modules at the given location will
                     be imported except for ``__init__``.
    :type location: str or None
    :param attr: if specified, this attribute is fetched from all modules loaded
                 from the specified ``location`` and used as the value in the
                 plugin dict; if not specified, the module itself is used as the
                 module.
    :type attr: str or None
    :param filter: an optional callable to which all candidate module names
                   found at ``location`` will be passed. If the filter returns
                   a zero value that module will skipped, otherwise it will be
                   imported; if the filter returns a string, it specifies the
                   name of a module to be imported instead.
    :type filter: callable or None
    :param scope: the global scope to use when importing plugin modules.  If
                  this is not specified, the caller's scope will be used (using
                  stack inspection trickery).  If get_plugins() is being called
                  indirectly, you will need to pass through the value of
                  globals().
    :type scope: dict
    :returns: a dict mapping plugin names to plugin objects.

    If a plugin raises an exception while it is being imported, the value in
    the returned dictionary for that plugin will be the Exception object that
    was raised.

    Plugins can be loaded by one or both of the following methods:
        #. Loading modules from a specified directory location either on-disk
           or inside a zipped egg
        #. Entry point groups offered by setuptools

    Method 1 is the more traditional approach to plugins in Python.  For example,
    you might have a directory structure like::

        module/
            plugins/
                __init__.py
                plugin1.py
                plugin2.py

    There is typically logic inside ``plugins/__init__.py`` to import all other
    files in the same directory (``os.path.dirname(__file__)``).  It can pass
    ``location=__file__`` to ``kaa.utils.get_plugins()`` to do this:

        >>> kaa.utils.get_plugins(location=__file__)
        {'plugin1': <module 'module.plugins.plugin1' from '.../module/plugins/plugin1.py'>, 
         'plugin2': <module 'module.plugins.plugin2' from '.../module/plugins/plugin2.py'>}

    .. note::
       Plugins will be imported relative to the caller's module name.  So if
       the caller is module ``module.plugins``, plugin1 will be imported as
       ``module.plugins.plugin1``.

    This also works when ``plugins/__init__.py`` is inside a zipped egg.
    However, if you reference ``__file__`` as above, setuptools (assuming it's
    available on the system), when it examines the source code during
    installation, will think it's not safe to zip the tree into an egg file,
    and so it will install it as an on-disk directory tree instead.  You can
    override this by passing ``zip_safe=True`` to ``setup()`` in the setup
    script for the application that loads plugins.

    Method 2 is the approach to take when setuptools is available.  Plugin modules
    register themselves with an entry point group name.  For example, a plugin
    module's ``setup.py`` may call ``setup()`` with::

        setup(
            module='myplugin',
            # ...
            plugins = {'someapplication.plugins': 'src/submodule'},
            entry_points = {'someapplication.plugins': 'plugname = myplugin.submodule:SomeClass'},
        )

    When SomeApplication wants to load all registered modules, it can do::

        >>> kaa.utils.get_plugins(group='someapplication.plugins')
        {'plugname': <class myplugin.submodule.SomeClass at 0xdeadbeef>}

    The ``entry_points`` kwarg specified in the plugin module can specify multiple
    plugin names, and ``SomeClass`` could be any python object, as long as it is
    exposed within ``myplugin.submodule``.  For more information on entry
    points, refer to `setuptools documentation
    <http://peak.telecommunity.com/DevCenter/PkgResources#entry-points>`_.

    It is possible and in fact recommended to mix both methods.  (If your
    project makes setuptools a mandatory dependency, then you can use entry
    point groups exclusively.)  If ``group`` is not specified, then it becomes
    impossible for either the application or the plugin to be installed as
    eggs.

    The ``attr`` kwarg makes it more convenient to combine both methods.
    Plugins loaded from entry points will be SomeClass objects.  If (assuming
    now the application is not installed as a zipped egg but rather an on-disk
    source tree) some plugins were installed to the applications source tree,
    while others installed as eggs registered with the entry point group,
    you would want to pull ``SomeClass`` from those modules loaded from
    ``location``.  So::
    
        >>> kaa.utils.get_plugins(group='someapplication.plugins', location=__file__, attr='SomeClass')
        {'plugin1': <class module.plugins.plugin1.SomeClass at 0xdeadbeef>,
         'plugin2': <class module.plugins.plugin2.SomeClass at 0xcafebabe>,
         'plugname': <class myplugin.submodule.SomeClass at 0xbaadf00d>}

    """
    plugins = {}

    # If a path is specified, fetch all modules at the same level as the
    # given path.  This can also look into .egg files.
    if location:
        if os.path.splitext(location)[1] in ('.py', '.pyc', '.pyo'):
            # location is a file, so take the directory (probably __file__ was passed)
            location = os.path.dirname(location)
        if not location.endswith('/'):
            # Ensure location has a trialing /
            location += '/'

        if os.path.isdir(location):
            # location is an on-disk source tree.
            ls = os.listdir(location)
        elif '.egg/' in location:
            # location is an egg zip file.
            subpath = location.split('.egg/')[1]
            zip = zipimport.zipimporter(location)
            # Fetch list of all files inside the egg at the same level as the
            # given location.
            ls = [k[len(subpath):] for k in zip._files.keys() \
                                   if k.startswith(subpath) and k.count('/') == subpath.count('/')]

        # Insert the normalized location into the front of sys.path so that when we import
        # the plugins the location is searched first.
        sys.path.insert(0, location)
        try:
            for fname in ls:
                name, ext = os.path.splitext(fname)
                # don't attempt to import if file is not a python file or
                # directory.  Also, skip __init__.py from the location dir.
                if ext not in ('.py', 'pyc', '.pyo', '') or fname.startswith('__init__.py'):
                    continue

                allowed = filter(name) if filter else name
                # if filter returned a string, use that as module name.
                name = allowed if isinstance(allowed, basestring) else name
                if not allowed or name in plugins:
                    # filter returned zero value.
                    continue

                # Now we try to import the module
                try:
                    try:
                        # Import using the global scope of the caller, so
                        # that if the caller is module kaa.foo.bar, plugin
                        # baz is imported as kaa.foo.baz.
                        globs = scope or inspect.currentframe().f_back.f_globals
                    except (TypeError, KeyError):
                        globs = None
                    mod = __import__(name, globs)
                except Exception, e:
                    # Pass the exception back.
                    plugins[name] = e
                else:
                    # Import successful, add it to the plugins dict.
                    plugins[name] = getattr(mod, attr) if attr else mod
        finally:
            sys.path.pop(0)

    # If an entry point group was specified, fetch all entry points for that
    # group and load them into the plugins dict.
    if group:
        try:
            import pkg_resources
        except ImportError:
            # No setuptools.
            pass
        else:
            # Fetch a list of all entry points (defined as entry_points kwarg passed to
            # setup() for plugin modules) and load them, which returns the Plugin class
            # they were registered with.
            for entrypoint in pkg_resources.iter_entry_points(group):
                try:
                    plugins[entrypoint.name] = entrypoint.load()
                except Exception, e:
                    # Load failed, pass the exception back.
                    plugins[entrypoint.name] = e


    return plugins



# FIXME: this is not really what a Singleton is.  This should be called LazyObject
# or something.
class Singleton(object):
    """
    Create Singleton object from classref on demand.
    """

    class MemberFunction(object):
        def __init__(self, singleton, name):
            self._singleton = singleton
            self._name = name

        def __call__(self, *args, **kwargs):
            return getattr(self._singleton(), self._name)(*args, **kwargs)


    def __init__(self, classref):
        self._singleton = None
        self._class = classref

    def __call__(self):
        if self._singleton is None:
            self._singleton = self._class()
        return self._singleton

    def __getattr__(self, attr):
        if self._singleton is None:
            return Singleton.MemberFunction(self, attr)
        return getattr(self._singleton, attr)


# Python 2.6 and later has the enhanced property decorator (supports
# setters and deleters), but earlier versions don't, so for < 2.6
# we replace the built-in property to mimic the behaviour in 2.6+.
if sys.hexversion >= 0x02060000:
    # Bind built-in property to global name that we export.  So for 2.6+
    # kaa.utils.property is the built-in property.
    property = property
else:

    class property(property):
        """
        Replaces built-in property function to extend it as per
        http://bugs.python.org/issue1416
        """
        def __init__(self, fget = None, fset = None, fdel = None, doc = None):
            super(property, self).__init__(fget, fset, fdel)
            self.__doc__ = doc or fget.__doc__

        def _add_doc(self, prop, doc = None):
            prop.__doc__ = doc or self.__doc__
            return prop

        def setter(self, fset):
            if isinstance(fset, property):
                # Wrapping another property, use deleter.
                self, fset = fset, fset.fdel
            return self._add_doc(property(self.fget, fset, self.fdel))

        def deleter(self, fdel):
            if isinstance(fdel, property):
                # Wrapping another property, use setter.
                self, fdel = fdel, fdel.fset
            return self._add_doc(property(self.fget, self.fset, fdel))

        def getter(self, fget):
            return self._add_doc(property(fget, self.fset, self.fdel), fget.__doc__ or self.fget.__doc__)


def wraps(origfunc, lshift=0):
    """
    Decorator factory: used to create a decorator that assumes the same
    attributes (name, docstring, signature) as its decorated function when
    sphinx has been imported.  This is necessary because sphinx uses
    introspection to construct the documentation.

    This logic is inspired from Michele Simionato's decorator module.

        >>> def decorator(func):
        ...     @wraps(func)
        ...     def newfunc(*args, **kwargs):
        ...             # custom logic here ...
        ...             return func(*args, **kwargs)
        ...     return newfunc

    @param origfunc: the original function being decorated which is to be
        wrapped.
    @param lshift: number of arguments to shift from the left of the original
        function's call spec.  Wrapped function will have this nubmer of
        arguments removed.
    @return: a decorator which has the attributes of the decorated function.
    """
    if 'sphinx.builders' not in sys.modules:
        # sphinx not imported, so return a decorator that passes the func through.
        return functools.wraps(origfunc)
    elif lshift == 0:
        # Simple case, we don't need to munge args, so we can pass origfunc.
        return lambda func: origfunc

    # The idea here is to turn an origfunc with a signature like:
    #    origfunc(progress, a, b, c=42, *args, **kwargs)
    # into:
    #    lambda a, b, c=42, *args, **kwargs: log.error("...")
    spec = list(inspect.getargspec(origfunc))

    # Wrapped function needs a different signature.  Currently we can just
    # shift from the left of the args (e.g. for kaa.threaded progress arg).
    # FIXME: doesn't work if the shifted arg is a kwarg.
    spec[0] = spec[0][lshift:]

    if spec[-1]:
        # For the lambda signature's kwarg defaults, remap them into values
        # that can be referenced from the eval's local scope.  Otherwise only
        # intrinsics could be used as kwarg defaults.
        # Preserve old kwarg value list, to be passed into eval's locals scope.
        kwarg_values = spec[-1]
        # Changes (a=1, b=Foo) to a='__kaa_kw_defs[1]', b='__kaa_kw_defs[2]'
        sigspec = spec[:3] + [[ '__kaa_kw_defs[%d]' % n for n in range(len(spec[-1])) ]]
        sig = inspect.formatargspec(*sigspec)[1:-1]
        # Removes the quotes between __kaa_kw_defs[x]
        sig = re.sub(r"'(__kaa_kw_defs\[\d+\])'", '\\1', sig)
    else:
        sig = inspect.formatargspec(*spec)[1:-1]
        kwarg_values = None

    src = 'lambda %s: __kaa_log_.error("doc generation mode: decorated function \'%s\' was called")' % (sig, origfunc.__name__)
    def decorator(func):
        dec_func = eval(src, {'__kaa_log_': log, '__kaa_kw_defs': kwarg_values})
        return update_wrapper(dec_func, origfunc)
    return decorator


class DecoratorDataStore(object):
    """
    A utility class for decorators that sets or gets a value to/from a
    decorated function.  Attributes of instances of this class can be get, set,
    or deleted, and those attributes are associated with the decorated
    function.

    The object to which the data is attached is either the function itself for
    non-method, or the instance object for methods.

    There are two possible perspectives of using the data store: from inside
    the decorator, and from outside the decorator.  This allows, for example, a
    method to access data stored by one of its decorators.
    """
    def __init__(self, func, newfunc=None, newfunc_args=None, identifier=None):
        # Object the data will be stored in.
        target = func
        if hasattr(func, 'im_self'):
            # Data store requested for a specific method.
            target = func.im_self

        # This kludge compares the code object of newfunc (this wrapper) with the
        # code object of the first argument's attribute of the function's name.  If
        # they're the same, then we must be decorating a method, and we can attach
        # the timer object to the instance instead of the function.
        method = newfunc_args and getattr(newfunc_args[0], func.func_name, None)
        if method and newfunc.func_code == method.func_code:
            # Decorated function is a method, so store data in the instance.
            target = newfunc_args[0]

        self.__target = target
        # FIXME: user identifier only works when decorating methods.
        self.__name = identifier if identifier else func.func_name

    def __hash(self, key):
        return '__kaa_decorator_data_%s_%s' % (key, self.__name)

    def __getattr__(self, key):
        if key.startswith('_DecoratorDataStore__'):
            return super(DecoratorDataStore, self).__getattr__(key)
        return getattr(self.__target, self.__hash(key))

    def __setattr__(self, key, value):
        if key.startswith('_DecoratorDataStore__'):
            return super(DecoratorDataStore, self).__setattr__(key, value)
        return setattr(self.__target, self.__hash(key), value)

    def __hasattr__(self, key):
        return hasattr(self.__target, self.__hash(key))

    def __contains__(self, key):
        return hasattr(self.__target, self.__hash(key))

    def __delattr__(self, key):
        return delattr(self.__target, self.__hash(key))