root/masterdriverz/snakeoil/snakeoil/demandload.py @ masterdriverz%2540gentoo.org-20070430215448-vdqjns1gvqrqeyln

Revision masterdriverz%2540gentoo.org-20070430215448-vdqjns1gvqrqeyln, 8.3 kB (checked in by Charlie Shepherd <masterdriverz@…>, 21 months ago)

Pull pylint and {List,Tuple}BackedDict? improvements from my branch

Line 
1# Copyright: 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
2# Copyright: 2007 Marien Zwart <marienz@gentoo.org>
3# License: GPL2
4
5"""Demand load things when used.
6
7This uses L{Placeholder} objects which create an actual object on
8first use and know how to replace themselves with that object, so
9there is no performance penalty after first use.
10
11This trick is *mostly* transparent, but there are a few things you
12have to be careful with:
13
14 - You may not bind a second name to a placeholder object. Specifically,
15   if you demandload C{bar} in module C{foo}, you may not
16   C{from foo import bar} in a third module. The placeholder object
17   does not "know" it gets imported, so this does not trigger the
18   demandload: C{bar} in the third module is the placeholder object.
19   When that placeholder gets used it replaces itself with the actual
20   module in C{foo} but not in the third module.
21   Because this is normally unwanted (it introduces a small
22   performance hit) the placeholder object will raise an exception if
23   it detects this. But if the demandload gets triggered before the
24   third module is imported you do not get that exception, so you
25   have to be careful not to import or otherwise pass around the
26   placeholder object without triggering it.
27 - Not all operations on the placeholder object trigger demandload.
28   The most common problem is that C{except ExceptionClass} does not
29   work if C{ExceptionClass} is a placeholder.
30   C{except module.ExceptionClass} with C{module} a placeholder does
31   work. You can normally avoid this by always demandloading the
32   module, not something in it.
33"""
34
35# TODO: the use of a curried func instead of subclassing needs more thought.
36
37# the replace_func used by Placeholder is currently passed in as an
38# external callable, with "partial" used to provide arguments to it.
39# This works, but has the disadvantage that calling
40# demand_compile_regexp needs to import re (to hand re.compile to
41# partial). One way to avoid that would be to add a wrapper function
42# that delays the import (well, triggers the demandload) at the time
43# the regexp is used, but that's a bit convoluted. A different way is
44# to make replace_func a method of Placeholder implemented through
45# subclassing instead of a callable passed to its __init__. The
46# current version does not do this because getting/setting attributes
47# of Placeholder is annoying because of the
48# __getattribute__/__setattr__ override.
49
50
51from snakeoil.modules import load_any
52from snakeoil.currying import partial
53
54# There are some demandloaded imports below the definition of demandload.
55
56_allowed_chars = "".join((x.isalnum() or x in "_.") and " " or "a"
57    for x in map(chr, xrange(256)))
58
59def parse_imports(imports):
60    """Parse a sequence of strings describing imports.
61
62    For every input string it returns a tuple of (import, targetname).
63    Examples::
64
65      'foo' -> ('foo', 'foo')
66      'foo:bar' -> ('foo.bar', 'bar')
67      'foo:bar,baz@spork' -> ('foo.bar', 'bar'), ('foo.baz', 'spork')
68      'foo@bar' -> ('foo', 'bar')
69
70    Notice 'foo.bar' is not a valid input. This simplifies the code,
71    but if it is desired it can be added back.
72
73    @type  imports: sequence of C{str} objects.
74    @rtype: iterable of tuples of two C{str} objects.
75    """
76    for s in imports:
77        fromlist = s.split(':', 1)
78        if len(fromlist) == 1:
79            # Not a "from" import.
80            if '.' in s:
81                raise ValueError('dotted imports unsupported.')
82            split = s.split('@', 1)
83            for s in split:
84                if not s.translate(_allowed_chars).isspace():
85                    raise ValueError("bad target: %s" % s)
86            if len(split) == 2:
87                yield tuple(split)
88            else:
89                yield split[0], split[0]
90        else:
91            # "from" import.
92            base, targets = fromlist
93            if not base.translate(_allowed_chars).isspace():
94                raise ValueError("bad target: %s" % base)
95            for target in targets.split(','):
96                split = target.split('@', 1)
97                for s in split:
98                    if not s.translate(_allowed_chars).isspace():
99                        raise ValueError("bad target: %s" % s)
100                yield base + '.' + split[0], split[-1]
101
102
103class Placeholder(object):
104
105    """Object that knows how to replace itself when first accessed.
106
107    See the module docstring for common problems with its use.
108    """
109
110    def __init__(self, scope, name, replace_func):
111        """Initialize.
112
113        @param scope: the scope we live in, normally the result of
114          C{globals()}.
115        @param name: the name we have in C{scope}.
116        @param replace_func: callable returning the object to replace us with.
117        """
118        object.__setattr__(self, '_scope', scope)
119        object.__setattr__(self, '_name', name)
120        object.__setattr__(self, '_replace_func', replace_func)
121
122    def _already_replaced(self):
123        name = object.__getattribute__(self, '_name')
124        raise ValueError('Placeholder for %r was triggered twice' % (name,))
125
126    def _replace(self):
127        """Replace ourself in C{scope} with the result of our C{replace_func}.
128
129        @returns: the result of calling C{replace_func}.
130        """
131        replace_func = object.__getattribute__(self, '_replace_func')
132        scope = object.__getattribute__(self, '_scope')
133        name = object.__getattribute__(self, '_name')
134        # Paranoia, explained in the module docstring.
135        already_replaced = object.__getattribute__(self, '_already_replaced')
136        object.__setattr__(self, '_replace_func', already_replaced)
137
138        # Cleanup, possibly unnecessary.
139        object.__setattr__(self, '_scope', None)
140
141        result = replace_func()
142        scope[name] = result
143        return result
144
145    # Various methods proxied to our replacement.
146
147    def __str__(self):
148        return self.__getattribute__('__str__')()
149
150    def __getattribute__(self, attr):
151        result = object.__getattribute__(self, '_replace')()
152        return getattr(result, attr)
153
154    def __setattr__(self, attr, value):
155        result = object.__getattribute__(self, '_replace')()
156        setattr(result, attr, value)
157
158    def __call__(self, *args, **kwargs):
159        result = object.__getattribute__(self, '_replace')()
160        return result(*args, **kwargs)
161
162
163def demandload(scope, *imports):
164    """Import modules into scope when each is first used.
165
166    scope should be the value of C{globals()} in the module calling
167    this function. (using C{locals()} may work but is not recommended
168    since mutating that is not safe).
169
170    Other args are strings listing module names.
171    names are handled like this::
172
173      foo            import foo
174      foo@bar        import foo as bar
175      foo:bar        from foo import bar
176      foo:bar,quux   from foo import bar, quux
177      foo.bar:quux   from foo.bar import quux
178      foo:baz@quux   from foo import baz as quux
179    """
180    for source, target in parse_imports(imports):
181        scope[target] = Placeholder(scope, target, partial(load_any, source))
182
183
184demandload(globals(), 're')
185
186# Extra name to make undoing monkeypatching demandload with
187# disabled_demandload easier.
188enabled_demandload = demandload
189
190
191def disabled_demandload(scope, *imports):
192    """Exactly like L{demandload} but does all imports immediately."""
193    for source, target in parse_imports(imports):
194        scope[target] = load_any(source)
195
196
197class RegexPlaceholder(Placeholder):
198    """
199    Compiled Regex object that knows how to replace itself when first accessed.
200
201    See the module docstring for common problems with its use; used by
202    L{demand_compile_regexp}.
203    """
204
205    def _replace(self):
206        args, kwargs = object.__getattribute__(self, '_replace_func')
207        object.__setattr__(self, '_replace_func',
208            partial(re.compile, *args, **kwargs))
209        return Placeholder._replace(self)
210
211
212
213def demand_compile_regexp(scope, name, *args, **kwargs):
214    """Demandloaded version of L{re.compile}.
215
216    Extra arguments are passed unchanged to L{re.compile}.
217
218    This returns the placeholder, which you *must* bind to C{name} in
219    the scope you pass as C{scope}. It is done this way to prevent
220    confusing code analysis tools like pylint.
221
222    @param scope: the scope, just like for L{demandload}.
223    @param name: the name of the compiled re object in that scope.
224    @returns: the placeholder object.
225    """
226    return RegexPlaceholder(scope, name, (args, kwargs))
Note: See TracBrowser for help on using the browser.