| 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 | |
|---|
| 7 | This uses L{Placeholder} objects which create an actual object on |
|---|
| 8 | first use and know how to replace themselves with that object, so |
|---|
| 9 | there is no performance penalty after first use. |
|---|
| 10 | |
|---|
| 11 | This trick is *mostly* transparent, but there are a few things you |
|---|
| 12 | have 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 | |
|---|
| 51 | from snakeoil.modules import load_any |
|---|
| 52 | from 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 | |
|---|
| 59 | def 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 | |
|---|
| 103 | class 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 | |
|---|
| 163 | def 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 | |
|---|
| 184 | demandload(globals(), 're') |
|---|
| 185 | |
|---|
| 186 | # Extra name to make undoing monkeypatching demandload with |
|---|
| 187 | # disabled_demandload easier. |
|---|
| 188 | enabled_demandload = demandload |
|---|
| 189 | |
|---|
| 190 | |
|---|
| 191 | def 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 | |
|---|
| 197 | class 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 | |
|---|
| 213 | def 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)) |
|---|