Package tdi :: Module util
[frames] | no frames]

Source Code for Module tdi.util

  1  # -*- coding: ascii -*- 
  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 
43 44 45 -def _make_parse_content_type():
46 """ 47 Make content type parser 48 49 :Return: parse_content_type 50 :Rtype: ``callable`` 51 """ 52 # These are a bit more lenient than RFC 2045. 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): # pylint: disable = W0621 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()
108 109 110 -class Version(tuple):
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 # pylint: disable = W0613 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 # pylint: disable = W0613 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
184 - def __repr__(self):
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
199 - def __str__(self):
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
211 - def __unicode__(self):
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
220 221 -def find_public(space):
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
236 237 -def Property(func): # pylint: disable = C0103
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
261 262 -def decorating(decorated, extra=None):
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 # pylint: disable = R0912 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 # assign a name for the proxy function. 302 # Make sure it's not already used for something else (function 303 # name or argument) 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 # Compile wrapper function 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 # pylint: disable = W0122 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
340 341 -class Deprecator(object):
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 # pylint: disable = W0613 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) # pylint: disable = E1102
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
413 - def __getattr__(self, name):
414 """ Get attribute with deprecation warning """ 415 self.__warn() 416 return getattr(self.__todeprecate, name)
417
418 - def __iter__(self):
419 """ Get iterator with deprecation warning """ 420 self.__warn() 421 return iter(self.__todeprecate)
422
423 424 -class CallableDeprecator(Deprecator):
425 """ Callable proxy deprecation class """ 426
427 - def __call__(self, *args, **kwargs):
428 """ Call with deprecation warning """ 429 self._Deprecator__warn() 430 return self._Deprecator__todeprecate(*args, **kwargs)
431
432 433 -def load_dotted(name):
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
470 471 -def make_dotted(name):
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
505 506 -class DependencyGraph(object):
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
528 - def __init__(self):
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
557 - def resolve(self):
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() # ensure stable output 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() # ensure stable output 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() # ensure stable output 596 return result + leaves
597
598 - def _check_cycle(self, node):
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 # run a DFS for each child node until we find 615 # a) a leaf (then backtrack) 616 # b) node (cycle) 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