1
2 u"""
3 :Copyright:
4
5 Copyright 2006 - 2013
6 Andr\xe9 Malo or his licensors, as applicable
7
8 :License:
9
10 Licensed under the Apache License, Version 2.0 (the "License");
11 you may not use this file except in compliance with the License.
12 You may obtain a copy of the License at
13
14 http://www.apache.org/licenses/LICENSE-2.0
15
16 Unless required by applicable law or agreed to in writing, software
17 distributed under the License is distributed on an "AS IS" BASIS,
18 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 See the License for the specific language governing permissions and
20 limitations under the License.
21
22 ================
23 Misc Utilities
24 ================
25
26 Misc utilities.
27 """
28 __author__ = u"Andr\xe9 Malo"
29 __docformat__ = "restructuredtext en"
30
31 import collections as _collections
32 import imp as _imp
33 import inspect as _inspect
34 import operator as _op
35 import os as _os
36 import re as _re
37 import sys as _sys
38 import types as _types
39
40 from tdi import _exceptions
41
42 DependencyCycle = _exceptions.DependencyCycle
46 """
47 Make content type parser
48
49 :Return: parse_content_type
50 :Rtype: ``callable``
51 """
52
53 tokenres = r'[^\000-\040()<>@,;:\\"/[\]?=]+'
54 qcontent = r'[^\000\\"]'
55 qsres = r'"%(qc)s*(?:\\"%(qc)s*)*"' % {'qc': qcontent}
56 valueres = r'(?:%(token)s|%(quoted-string)s)' % {
57 'token': tokenres, 'quoted-string': qsres,
58 }
59
60 typere = _re.compile(
61 r'\s*([^;/\s]+/[^;/\s]+)((?:\s*;\s*%(key)s\s*=\s*%(val)s)*)\s*$' %
62 {'key': tokenres, 'val': valueres,}
63 )
64 pairre = _re.compile(r'\s*;\s*(%(key)s)\s*=\s*(%(val)s)' % {
65 'key': tokenres, 'val': valueres
66 })
67 stripre = _re.compile(r'\r?\n')
68
69 def parse_content_type(value):
70 """
71 Parse a content type
72
73 :Warning: comments are not recognized (yet?)
74
75 :Parameters:
76 `value` : ``basestring``
77 The value to parse - must be ascii compatible
78
79 :Return: The parsed header (``(value, {key, [value, value, ...]})``)
80 or ``None``
81 :Rtype: ``tuple``
82 """
83 try:
84 if isinstance(value, unicode):
85 value.encode('ascii')
86 else:
87 value.decode('ascii')
88 except (AttributeError, UnicodeError):
89 return None
90
91 match = typere.match(value)
92 if not match:
93 return None
94
95 parsed = (match.group(1).lower(), {})
96 match = match.group(2)
97 if match:
98 for key, val in pairre.findall(match):
99 if val[:1] == '"':
100 val = stripre.sub(r'', val[1:-1]).replace(r'\"', '"')
101 parsed[1].setdefault(key.lower(), []).append(val)
102
103 return parsed
104
105 return parse_content_type
106
107 parse_content_type = _make_parse_content_type()
111 """
112 Represents the package version
113
114 :IVariables:
115 `major` : ``int``
116 The major version number
117
118 `minor` : ``int``
119 The minor version number
120
121 `patch` : ``int``
122 The patch level version number
123
124 `is_dev` : ``bool``
125 Is it a development version?
126
127 `revision` : ``int``
128 Internal revision
129 """
130
131 - def __new__(cls, versionstring, is_dev, revision):
132 """
133 Construction
134
135 :Parameters:
136 `versionstring` : ``str``
137 The numbered version string (like ``"1.1.0"``)
138 It should contain at least three dot separated numbers
139
140 `is_dev` : ``bool``
141 Is it a development version?
142
143 `revision` : ``int``
144 Internal revision
145
146 :Return: New version instance
147 :Rtype: `version`
148 """
149
150 tup = []
151 versionstring = versionstring.strip()
152 if versionstring:
153 for item in versionstring.split('.'):
154 try:
155 item = int(item)
156 except ValueError:
157 pass
158 tup.append(item)
159 while len(tup) < 3:
160 tup.append(0)
161 return tuple.__new__(cls, tup)
162
163 - def __init__(self, versionstring, is_dev, revision):
164 """
165 Initialization
166
167 :Parameters:
168 `versionstring` : ``str``
169 The numbered version string (like ``1.1.0``)
170 It should contain at least three dot separated numbers
171
172 `is_dev` : ``bool``
173 Is it a development version?
174
175 `revision` : ``int``
176 Internal revision
177 """
178
179 super(Version, self).__init__()
180 self.major, self.minor, self.patch = self[:3]
181 self.is_dev = bool(is_dev)
182 self.revision = int(revision)
183
185 """
186 Create a development string representation
187
188 :Return: The string representation
189 :Rtype: ``str``
190 """
191 return "%s.%s(%r, is_dev=%r, revision=%r)" % (
192 self.__class__.__module__,
193 self.__class__.__name__,
194 ".".join(map(str, self)),
195 self.is_dev,
196 self.revision,
197 )
198
200 """
201 Create a version like string representation
202
203 :Return: The string representation
204 :Rtype: ``str``
205 """
206 return "%s%s" % (
207 ".".join(map(str, self)),
208 ("", "-dev-r%d" % self.revision)[self.is_dev],
209 )
210
212 """
213 Create a version like unicode representation
214
215 :Return: The unicode representation
216 :Rtype: ``unicode``
217 """
218 return str(self).decode('ascii')
219
222 """
223 Determine all public names in space
224
225 :Parameters:
226 `space` : ``dict``
227 Name space to inspect
228
229 :Return: List of public names
230 :Rtype: ``list``
231 """
232 if space.has_key('__all__'):
233 return list(space['__all__'])
234 return [key for key in space.keys() if not key.startswith('_')]
235
238 """
239 Property with improved docs handling
240
241 :Parameters:
242 `func` : ``callable``
243 The function providing the property parameters. It takes no arguments
244 as returns a dict containing the keyword arguments to be defined for
245 ``property``. The documentation is taken out the function by default,
246 but can be overridden in the returned dict.
247
248 :Return: The requested property
249 :Rtype: ``property``
250 """
251 kwargs = func()
252 kwargs.setdefault('doc', func.__doc__)
253 kwargs = kwargs.get
254 return property(
255 fget=kwargs('fget'),
256 fset=kwargs('fset'),
257 fdel=kwargs('fdel'),
258 doc=kwargs('doc'),
259 )
260
263 """
264 Create decorator for designating decorators.
265
266 :Parameters:
267 `decorated` : function
268 Function to decorate
269
270 `extra` : ``dict``
271 Dict of consumed keyword parameters (not existing in the originally
272 decorated function), mapping to their defaults. If omitted or
273 ``None``, no extra keyword parameters are consumed. The arguments
274 must be consumed by the actual decorator function.
275
276 :Return: Decorator
277 :Rtype: ``callable``
278 """
279
280 def flat_names(args):
281 """ Create flat list of argument names """
282 for arg in args:
283 if isinstance(arg, basestring):
284 yield arg
285 else:
286 for arg in flat_names(arg):
287 yield arg
288 name = decorated.__name__
289 try:
290 dargspec = argspec = _inspect.getargspec(decorated)
291 except TypeError:
292 dargspec = argspec = ([], 'args', 'kwargs', None)
293 if extra:
294 keys = extra.keys()
295 argspec[0].extend(keys)
296 defaults = list(argspec[3] or ())
297 for key in keys:
298 defaults.append(extra[key])
299 argspec = (argspec[0], argspec[1], argspec[2], defaults)
300
301
302
303
304 counter, proxy_name = -1, 'proxy'
305 names = dict.fromkeys(flat_names(argspec[0]))
306 names[name] = None
307 while proxy_name in names:
308 counter += 1
309 proxy_name = 'proxy%s' % counter
310
311 def inner(decorator):
312 """ Actual decorator """
313
314 space = {proxy_name: decorator}
315 if argspec[3]:
316 kwnames = argspec[0][-len(argspec[3]):]
317 else:
318 kwnames = None
319 passed = _inspect.formatargspec(argspec[0], argspec[1], argspec[2],
320 kwnames, formatvalue=lambda value: '=' + value
321 )
322
323 exec "def %s%s: return %s%s" % (
324 name, _inspect.formatargspec(*argspec), proxy_name, passed
325 ) in space
326 wrapper = space[name]
327 wrapper.__dict__ = decorated.__dict__
328 wrapper.__doc__ = decorated.__doc__
329 if extra and decorated.__doc__ is not None:
330 if not decorated.__doc__.startswith('%s(' % name):
331 wrapper.__doc__ = "%s%s\n\n%s" % (
332 name,
333 _inspect.formatargspec(*dargspec),
334 decorated.__doc__,
335 )
336 return wrapper
337
338 return inner
339
342 """
343 Deprecation proxy class
344
345 The class basically emits a deprecation warning on access.
346
347 :IVariables:
348 `__todeprecate` : any
349 Object to deprecate
350
351 `__warn` : ``callable``
352 Warn function
353 """
354 - def __new__(cls, todeprecate, message=None):
355 """
356 Construct
357
358 :Parameters:
359 `todeprecate` : any
360 Object to deprecate
361
362 `message` : ``str``
363 Custom message. If omitted or ``None``, a default message is
364 generated.
365
366 :Return: Deprecator instance
367 :Rtype: `Deprecator`
368 """
369
370 if type(todeprecate) is _types.MethodType:
371 call = cls(todeprecate.im_func, message=message)
372 @decorating(todeprecate.im_func)
373 def func(*args, **kwargs):
374 """ Wrapper to build a new method """
375 return call(*args, **kwargs)
376 return _types.MethodType(func, None, todeprecate.im_class)
377 elif cls == Deprecator and callable(todeprecate):
378 res = CallableDeprecator(todeprecate, message=message)
379 if type(todeprecate) is _types.FunctionType:
380 res = decorating(todeprecate)(res)
381 return res
382 return object.__new__(cls)
383
384 - def __init__(self, todeprecate, message=None):
385 """
386 Initialization
387
388 :Parameters:
389 `todeprecate` : any
390 Object to deprecate
391
392 `message` : ``str``
393 Custom message. If omitted or ``None``, a default message is
394 generated.
395 """
396 self.__todeprecate = todeprecate
397 if message is None:
398 if type(todeprecate) is _types.FunctionType:
399 name = todeprecate.__name__
400 else:
401 name = todeprecate.__class__.__name__
402 message = "%s.%s is deprecated." % (todeprecate.__module__, name)
403 if _os.environ.get('EPYDOC_INSPECTOR') == '1':
404 def warn():
405 """ Dummy to not clutter epydoc output """
406 pass
407 else:
408 def warn():
409 """ Emit the message """
410 _exceptions.DeprecationWarning.emit(message, stacklevel=3)
411 self.__warn = warn
412
414 """ Get attribute with deprecation warning """
415 self.__warn()
416 return getattr(self.__todeprecate, name)
417
419 """ Get iterator with deprecation warning """
420 self.__warn()
421 return iter(self.__todeprecate)
422
425 """ Callable proxy deprecation class """
426
428 """ Call with deprecation warning """
429 self._Deprecator__warn()
430 return self._Deprecator__todeprecate(*args, **kwargs)
431
434 """
435 Load a dotted name
436
437 The dotted name can be anything, which is passively resolvable
438 (i.e. without the invocation of a class to get their attributes or
439 the like). For example, `name` could be 'tdi.util.load_dotted'
440 and would return this very function. It's assumed that the first
441 part of the `name` is always is a module.
442
443 :Parameters:
444 `name` : ``str``
445 The dotted name to load
446
447 :Return: The loaded object
448 :Rtype: any
449
450 :Exceptions:
451 - `ImportError` : A module in the path could not be loaded
452 """
453 components = name.split('.')
454 path = [components.pop(0)]
455 obj = __import__(path[0])
456 while components:
457 comp = components.pop(0)
458 path.append(comp)
459 try:
460 obj = getattr(obj, comp)
461 except AttributeError:
462 __import__('.'.join(path))
463 try:
464 obj = getattr(obj, comp)
465 except AttributeError:
466 raise ImportError('.'.join(path))
467
468 return obj
469
472 """
473 Generate a dotted module
474
475 :Parameters:
476 `name` : ``str``
477 Fully qualified module name (like ``tdi.util``)
478
479 :Return: The module object of the last part and the information whether
480 the last part was newly added (``(module, bool)``)
481 :Rtype: ``tuple``
482
483 :Exceptions:
484 - `ImportError` : The module name was horribly invalid
485 """
486 sofar, parts = [], name.split('.')
487 oldmod = None
488 for part in parts:
489 if not part:
490 raise ImportError("Invalid module name %r" % (name,))
491 partname = ".".join(sofar + [part])
492 try:
493 fresh, mod = False, load_dotted(partname)
494 except ImportError:
495 mod = _imp.new_module(partname)
496 mod.__path__ = []
497 fresh = mod == _sys.modules.setdefault(partname, mod)
498 if oldmod is not None:
499 setattr(oldmod, part, mod)
500 oldmod = mod
501 sofar.append(part)
502
503 return mod, fresh
504
507 """
508 Dependency Graph Container
509
510 This is a simple directed acyclic graph. The graph starts empty, and new
511 nodes (and edges) are added using the `add` method. If the newly added
512 create a cycle, an exception is thrown.
513
514 Finally, the graph is resolved using the `resolve` method. The method will
515 return topologically ordered nodes and destroy the graph. The topological
516 order is *stable*, meaning, the same graph will always produce the same
517 output.
518
519 :IVariables:
520 `_outgoing` : ``dict``
521 Mapping of outgoing nodes (node -> set(outgoing neighbours))
522
523 `_incoming` : ``dict``
524 Mapping of incoming nodes (node -> set(incoming neighbours))
525 """
526 __slots__ = ('_outgoing', '_incoming')
527
529 """ Initialization """
530 self._outgoing = {}
531 self._incoming = {}
532
533 - def add(self, start, end):
534 """
535 Add a new nodes with edge to the graph
536
537 The edge is directed from `start` to `end`.
538
539 :Parameters:
540 `start` : ``str``
541 Node
542
543 `end` : ``str``
544 Node
545 """
546 outgoing, incoming = self._outgoing, self._incoming
547 if start not in outgoing:
548 outgoing[start] = set()
549 outgoing[start].add(end)
550
551 if end not in incoming:
552 incoming[end] = set()
553 incoming[end].add(start)
554
555 self._check_cycle(end)
556
558 """
559 Resolve graph and return nodes in topological order
560
561 The graph is defined by outgoing and incoming dicts (mapping nodes to
562 their outgoing or incoming neighbours). The graph is destroyed in the
563 process.
564
565 :Return: Sorted node list. The output is stable, because nodes on
566 the same level are sorted alphabetically. Furthermore all
567 leave nodes are put at the end.
568 :Rtype: ``list``
569 """
570 result, outgoing, incoming = [], self._outgoing, self._incoming
571 roots = list(set(outgoing.iterkeys()) - set(incoming.iterkeys()))
572 leaves = set(incoming.iterkeys()) - set(outgoing.iterkeys())
573
574 roots.sort()
575 roots = _collections.deque(roots)
576 roots_push, roots_pop = roots.appendleft, roots.pop
577 result_push, opop, ipop = result.append, outgoing.pop, incoming.pop
578 while roots:
579 node = roots_pop()
580 if node not in leaves:
581 result_push(node)
582 children = list(opop(node, ()))
583 children.sort()
584 for child in children:
585 parents = incoming[child]
586 parents.remove(node)
587 if not parents:
588 roots_push(child)
589 ipop(child)
590
591 if outgoing or incoming:
592 raise AssertionError("Graph not resolved (this is a bug).")
593
594 leaves = list(leaves)
595 leaves.sort()
596 return result + leaves
597
599 """
600 Find a cycle containing `node`
601
602 This assumes, that there's no other possible cycle in the graph. This
603 assumption is valid, because the graph is checked whenever a new
604 edge is added.
605
606 :Parameters:
607 `node` : ``str``
608 Node which may be part of a cycle.
609
610 :Exceptions:
611 - `DependencyCycle` : Raised, if there is, indeed, a cycle in the
612 graph. The cycling nodes are passed as a list to the exception.
613 """
614
615
616
617 outgoing = self._outgoing
618 if node in outgoing:
619 iter_ = iter
620 stack = [(node, iter_(outgoing[node]).next)]
621 exhausted, push, pop = StopIteration, stack.append, stack.pop
622
623 while stack:
624 try:
625 child = stack[-1][1]()
626 except exhausted:
627 pop()
628 else:
629 if child == node:
630 raise DependencyCycle(map(_op.itemgetter(0), stack))
631 elif child in outgoing:
632 push((child, iter_(outgoing[child]).next))
633