1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Common Utilities
19 ================
20
21 Certain utilities to make the life more easy.
22 """
23 __author__ = u"Andr\xe9 Malo"
24 __docformat__ = "restructuredtext en"
25
26 import imp as _imp
27 import inspect as _inspect
28 import keyword as _keyword
29 import os as _os
30 import re as _re
31 import sys as _sys
32 import traceback as _traceback
33 import warnings as _warnings
34 import weakref as _weakref
35
36 from wtf import WtfWarning
37
38
40 """ A package import failed, but is configured to not be fatal """
41
42
44 """
45 Create decorator for designating decorators.
46
47 :Parameters:
48 `decorated` : function
49 Function to decorate
50
51 `extra` : ``dict``
52 Dict of consumed keyword parameters (not existing in the originally
53 decorated function), mapping to their defaults. If omitted or
54 ``None``, no extra keyword parameters are consumed. The arguments
55 must be consumed by the actual decorator function.
56
57 `skip` : ``int``
58 Skip positional parameters at the beginning
59
60 :Return: Decorator
61 :Rtype: ``callable``
62 """
63
64 idmatch = _re.compile(r'[a-zA-Z_][a-zA-Z_\d]*$').match
65 def flat_names(args):
66 """ Create flat list of argument names """
67 for arg in args:
68 if isinstance(arg, basestring):
69 yield arg
70 else:
71 for arg in flat_names(arg):
72 yield arg
73 try:
74 name = decorated.__name__
75 except AttributeError:
76 if isinstance(decorated, type):
77 name = decorated.__class__.__name__
78 decorated = decorated.__init__
79 else:
80 name = decorated.__class__.__name__
81 decorated = decorated.__call__
82 oname = name
83 if not idmatch(name) or _keyword.iskeyword(name):
84 name = 'unknown_name'
85
86 try:
87 dargspec = argspec = _inspect.getargspec(decorated)
88 except TypeError:
89 dargspec = argspec = ([], 'args', 'kwargs', None)
90 if skip:
91 argspec[0][:skip] = []
92 if extra:
93 keys = extra.keys()
94 argspec[0].extend(keys)
95 defaults = list(argspec[3] or ())
96 for key in keys:
97 defaults.append(extra[key])
98 argspec = (argspec[0], argspec[1], argspec[2], defaults)
99
100
101
102
103 counter, proxy_name = -1, 'proxy'
104 names = dict.fromkeys(flat_names(argspec[0]))
105 names[name] = None
106 while proxy_name in names:
107 counter += 1
108 proxy_name = 'proxy%s' % counter
109
110 def inner(decorator):
111 """ Actual decorator """
112
113 space = {proxy_name: decorator}
114 if argspec[3]:
115 kwnames = argspec[0][-len(argspec[3]):]
116 else:
117 kwnames = None
118 passed = _inspect.formatargspec(argspec[0], argspec[1], argspec[2],
119 kwnames, formatvalue=lambda value: '=' + value
120 )
121
122 exec "def %s%s: return %s%s" % (
123 name, _inspect.formatargspec(*argspec), proxy_name, passed
124 ) in space
125 wrapper = space[name]
126 wrapper.__dict__ = decorated.__dict__
127 wrapper.__doc__ = decorated.__doc__
128 if extra and decorated.__doc__ is not None:
129 if not decorated.__doc__.startswith('%s(' % oname):
130 wrapper.__doc__ = "%s%s\n\n%s" % (
131 oname,
132 _inspect.formatargspec(*dargspec),
133 decorated.__doc__,
134 )
135 return wrapper
136 return inner
137
138
140 """
141 Find argument position and create arg setter:
142
143 The signature of the setter function is::
144
145 def setarg(args, kwargs, value_func):
146 '''
147 Set argument
148
149 The argument is set only if the `value_func` function says so::
150
151 def value_func(oldval):
152 '''
153 Determine argument value
154
155 :Parameters:
156 `oldval` : any
157 Value passed in
158
159 :Return: A tuple containing a boolean if the value is
160 new and should be set (vs. to leave the
161 passed-in value) and the final value
162 :Rtype: ``tuple``
163 '''
164
165 :Parameters:
166 `args` : sequence
167 Positional arguments
168
169 `kwargs` : ``dict``
170 Keyword arguments
171
172 `value_func` : ``callable``
173 Value function
174
175 :Return: A tuple containing `value_func`'s return value, the
176 new args and the new kwargs.
177 :Rtype: ``tuple``
178 '''
179
180 :Parameters:
181 `arg` : ``str``
182 Argument name
183
184 `func` : ``callable``
185 Function to call
186
187 :Return: arg setter function(args, kwargs, value_func)
188 :Rtype: ``callable``
189 """
190 try:
191 spec = _inspect.getargspec(func)
192 except TypeError:
193 try:
194 if isinstance(func, type):
195 func = func.__init__
196 else:
197 func = func.__call__
198 except AttributeError:
199 spec = None
200 try:
201 spec = _inspect.getargspec(func)
202 except TypeError:
203 spec = None
204
205 if spec is not None and arg in spec[0]:
206 idx = spec[0].index(arg)
207 else:
208 idx = None
209
210 def setarg(args, kwargs, value_func):
211 """
212 Set argument
213
214 The argument is set only if the `value_func` function says so::
215
216 def value_func(oldval):
217 '''
218 Determine argument value
219
220 :Parameters:
221 `oldval` : any
222 Value passed in
223
224 :Return: A tuple containing a boolean if the value is new and
225 should be set (vs. to leave the passed-in value) and
226 the final value
227 :Rtype: ``tuple``
228 '''
229
230 :Parameters:
231 `args` : sequence
232 Positional arguments
233
234 `kwargs` : ``dict``
235 Keyword arguments
236
237 `value_func` : ``callable``
238 Value function
239
240 :Return: A tuple containing `value_func`'s return value, the new args
241 and the new kwargs.
242 :Rtype: ``tuple``
243 """
244 if idx is None or idx >= len(args):
245 kwargs = kwargs.copy()
246 created, value = value_func(kwargs.get(arg))
247 if created:
248 kwargs[arg] = value
249 else:
250 created, value = value_func(args[idx])
251 if created:
252 args = list(args)
253 args[idx] = value
254 args = tuple(args)
255 return (created, value), args, kwargs
256
257 return setarg
258
259
261 """
262 Load a dotted name
263
264 The dotted name can be anything, which is passively resolvable
265 (i.e. without the invocation of a class to get their attributes or
266 the like). For example, `name` could be 'wtf.util.load_dotted'
267 and would return this very function. It's assumed that the first
268 part of the `name` is always is a module.
269
270 :Parameters:
271 - `name`: The dotted name to load
272
273 :Types:
274 - `name`: ``str``
275
276 :return: The loaded object
277 :rtype: any
278
279 :Exceptions:
280 - `ImportError`: A module in the path could not be loaded
281 """
282 components = name.split('.')
283 path = [components.pop(0)]
284 obj = __import__(path[0])
285 while components:
286 comp = components.pop(0)
287 path.append(comp)
288 try:
289 obj = getattr(obj, comp)
290 except AttributeError:
291 __import__('.'.join(path))
292 try:
293 obj = getattr(obj, comp)
294 except AttributeError:
295 raise ImportError('.'.join(path))
296
297 return obj
298
299
301 """
302 Generate a dotted module
303
304 :Parameters:
305 - `name`: Fully qualified module name (like `wtf.services`)
306
307 :Types:
308 - `name`: ``str``
309
310 :return: The module object of the last part and the information whether
311 the last part was newly added (``(module, bool)``)
312 :rtype: ``tuple``
313
314 :Exceptions:
315 - `ImportError`: The module name was horribly invalid
316 """
317 sofar, parts = [], name.split('.')
318 oldmod = None
319 for part in parts:
320 if not part:
321 raise ImportError("Invalid module name %r" % (name,))
322 partname = ".".join(sofar + [part])
323 try:
324 fresh, mod = False, load_dotted(partname)
325 except ImportError:
326 mod = _imp.new_module(partname)
327 mod.__path__ = []
328 fresh = mod == _sys.modules.setdefault(partname, mod)
329 if oldmod is not None:
330 setattr(oldmod, part, mod)
331 oldmod = mod
332 sofar.append(part)
333
334 return mod, fresh
335
336
338 """
339 Collect all modules and subpackages of `package` recursively
340
341 :Parameters:
342 - `package`: The package to inspect, if it's a string the string
343 is interpreted as a python package name and imported first. A failed
344 import of this package cannot be suppressed.
345 - `errors`: What should happen on ``ImportError``s during the crawling
346 process? The following values are recognized:
347 ``ignore``, ``warn``, ``error``
348
349 :Types:
350 - `package`: ``module`` or ``str``
351 - `errors`: ``str``
352
353 :return: Iterator over the modules/packages (including the root package)
354 :rtype: ``iterable``
355
356 :Exceptions:
357 - `ImportError`: some import failed
358 - `ValueError`: The `errors` value could nto be recognized
359 - `OSError`: Something bad happened while accessing the file system
360 """
361
362
363 self = walk_package
364
365 if isinstance(package, basestring):
366 package = load_dotted(package)
367
368 if errors == 'ignore':
369 errors = lambda: None
370 elif errors == 'warn':
371 def errors():
372 """ Emit an import warning """
373 _warnings.warn(''.join(
374 _traceback.format_exception_only(*_sys.exc_info()[:2]),
375 category=ImportWarning
376 ))
377 elif errors == 'error':
378 def errors():
379 """ raise the import error """
380 raise
381 else:
382 raise ValueError("`errors` value not recognized")
383
384 modre, pkgre = self.matchers
385 exts = [item[0] for item in _imp.get_suffixes()][::-1]
386 seen = set()
387 def collect(package):
388 """ Collect the package recursively alphabetically """
389 try:
390 paths = package.__path__
391 except AttributeError:
392 try:
393 paths = [_os.path.dirname(package.__file__)]
394 except AttributeError:
395 paths = []
396 yield package
397
398 for basedir in paths:
399 for name in sorted(_os.listdir(basedir)):
400 fullname = _os.path.join(basedir, name)
401
402
403 if _os.path.isdir(fullname):
404 match = pkgre(name)
405 if not match:
406 continue
407 pkgname = "%s.%s" % (
408 package.__name__, match.group('name'))
409 if pkgname in seen:
410 continue
411 seen.add(pkgname)
412
413 seen.add('%s.__init__' % pkgname)
414 try:
415 _imp.find_module('__init__', [fullname])
416 except ImportError:
417
418 continue
419 else:
420 try:
421 pkg = __import__(pkgname, {}, {}, ['*'])
422 except ImportError:
423 errors()
424 continue
425 else:
426 for item in collect(pkg):
427 yield item
428
429
430 elif _os.path.isfile(fullname):
431 match = modre(name)
432 if match:
433 modname = match.group('name')
434 for ext in exts:
435 if modname.endswith(ext):
436 modname = "%s.%s" % (
437 package.__name__, modname[:-len(ext)]
438 )
439 break
440 else:
441 continue
442 if modname in seen:
443 continue
444 seen.add(modname)
445 try:
446 mod = __import__(modname, {}, {}, ['*'])
447 except ImportError:
448 errors()
449 continue
450 else:
451 yield mod
452 return collect(package)
453 walk_package.matchers = (
454 _re.compile(
455 r'(?P<name>[a-zA-Z_][a-zA-Z\d_]*(?:\.[^.]+)?)$'
456 ).match,
457 _re.compile(r'(?P<name>[a-zA-Z_][a-zA-Z\d_]*)$').match,
458 )
459
460
461 hpre = (
462 _re.compile(ur'(?P<ip>[^:]+|\[[^\]]+])(?::(?P<port>\d+))?$').match,
463 _re.compile(ur'(?:(?P<ip>[^:]+|\[[^\]]+]|\*):)?(?P<port>\d+)$').match,
464 _re.compile(ur'(?P<ip>[^:]+|\[[^\]]+]|\*)(?::(?P<port>\d+))?$').match,
465 )
467 """
468 Parse a socket specification
469
470 This is either ``u'host:port'`` or ``u'/foo/bar'``. The latter (`spec`
471 containing a slash) specifies a UNIX domain socket and will be
472 transformed to a string according to the inherited locale setting.
473 It may be a string initially, too.
474
475 For internet sockets, the port is optional (will be `default_port` then).
476 If `any` is true, the host may point to ``ANY``. That is: the host is
477 ``u'*'`` or the port stands completely alone (e.g. ``u'80'``).
478 Hostnames will be IDNA encoded.
479
480 :Parameters:
481 - `spec`: The socket spec
482 - `default_port`: The default port to apply
483 - `any`: Allow host resolve to ``ANY``?
484
485 :Types:
486 - `spec`: ``basestring``
487 - `default_port`: ``int``
488 - `any`: ``bool``
489
490 :return: The determined spec. It may be a string (for UNIX sockets) or a
491 tuple of host and port for internet sockets. (``('host', port)``)
492 :rtype: ``tuple`` or ``str``
493
494 :Exceptions:
495 - `ValueError`: Unparsable spec
496 """
497
498
499 if isinstance(spec, str):
500 if '/' in spec:
501 return spec
502 spec = spec.decode('ascii')
503 elif u'/' in spec:
504 encoding = _sys.getfilesystemencoding() or 'utf-8'
505 return spec.encode(encoding)
506
507 match = _hpre[bool(any)](spec)
508 if match is None and any:
509 match = _hpre[2](spec)
510 if match is None:
511 raise ValueError("Unrecognized socket spec: %r" % (spec,))
512 host, port = match.group('ip', 'port')
513 if any:
514 if not host or host == u'*':
515 host = None
516 if host is not None:
517 host = host.encode('idna')
518 if host.startswith('[') and host.endswith(']'):
519 host = host[1:-1]
520 if not port:
521 port = default_port
522 else:
523 port = int(port)
524 return (host, port)
525
526 del hpre
527
528
530 """
531 Base decorator class
532
533 Implement the `__call__` method in order to add some action.
534
535 :IVariables:
536 - `_func`: The decorated function
537 - `__name__`: The "official" name of the function with decorator
538 - `__doc__`: Function's doc string
539
540 :Types:
541 - `_func`: ``callable``
542 - `__name__`: ``str``
543 - `__doc__`: ``basestring``
544 """
545
547 """
548 Initialization
549
550 :Parameters:
551 - `func`: The callable to decorate
552
553 :Types:
554 - `func`: ``callable``
555 """
556 self._func = func
557 try:
558 name = func.__name__
559 except AttributeError:
560 name = repr(func)
561 self.__name__ = "@%s(%s)" % (self.__class__.__name__, name)
562 self.__doc__ = func.__doc__
563
565 """
566 Generic attribute getter (descriptor protocol)
567
568 :Parameters:
569 - `inst`: Object instance
570 - `owner`: Object owner
571
572 :Types:
573 - `inst`: ``object``
574 - `owner`: ``type``
575
576 :return: Proxy function which acts for the wrapped method
577 :rtype: ``callable``
578 """
579 def proxy(*args, **kwargs):
580 """ Method proxy """
581 return self(inst, *args, **kwargs)
582 proxy.__name__ = self.__name__
583 proxy.__dict__ = self._func.__dict__
584 try:
585 proxy.__doc__ = self._func.__doc__
586 except AttributeError:
587 pass
588 return proxy
589
591 """
592 Pass attribute requests to the function
593
594 :Parameters:
595 - `name`: The name of the attribute
596
597 :Types:
598 - `name`: ``str``
599
600 :return: The function attribute
601 :rtype: any
602
603 :Exceptions:
604 - `AttributeError`: The attribute was not found
605 """
606 return getattr(self._func, name)
607
609 """
610 Actual decorating entry point
611
612 :Parameters:
613 - `args`: Positioned parameters
614 - `kwargs`: named parameters
615
616 :Types:
617 - `args`: ``tuple``
618 - `kwargs`: ``dict``
619
620 :return: The return value of the decorated function (maybe modified
621 or replaced by the decorator)
622 :rtype: any
623 """
624 raise NotImplementedError()
625
626
628 """ Interface for pooled objects """
629
631 """
632 Destroy the object
633
634 The method has to advise the pool to forget it. It should call the
635 ``del_obj`` method of the pool for that purpose. In order to achieve
636 that the object needs to store a reference to pool internally. In
637 order to avoid circular references it is wise to store the pool as
638 a weak reference (see ``weakref`` module).
639 """
640
641
643 """
644 Abstract pool of arbitrary objects
645
646 :CVariables:
647 - `_LOCK`: Locking class. By default this is ``threading.Lock``. However,
648 if the pooled object's initializer or destructor (``.destroy``) are
649 calling back into get_conn or put_conn, it should be ``RLock``.
650 - `_FORK_PROTECT`: Clear the pool, when the PID changes?
651
652 :IVariables:
653 - `_not_empty`: "not empty" condition
654 - `_not_full`: "not full" condition
655 - `_pool`: actual object pool
656 - `_maxout`: Hard maximum number of pooled objects to be handed out
657 - `_maxcached`: Maximum number of pooled objects to be cached
658 - `_obj`: References to handed out objects, Note that the objects need to
659 be hashable
660 - `_pid`: PID of currently handed out and cached objects
661
662 :Types:
663 - `_LOCK`: ``NoneType``
664 - `_FORK_PROTECT`: ``bool``
665 - `_not_empty`: ``threading.Condition``
666 - `_not_full`: ``threading.Condition``
667 - `_pool`: ``collections.deque``
668 - `_maxout`: ``int``
669 - `_maxcached`: ``int``
670 - `_obj`: ``dict``
671 - `_pid`: ``int``
672 """
673 _LOCK = None
674 _FORK_PROTECT, _pid = False, None
675
677 """ Initialization """
678 import collections as _collections
679 import threading as _threading
680
681 if self._LOCK is None:
682 lock = _threading.Lock()
683 else:
684 lock = self._LOCK()
685 self._not_empty = _threading.Condition(lock)
686 self._not_full = _threading.Condition(lock)
687 self._pool = _collections.deque()
688 maxout = max(0, int(maxout))
689 if maxout:
690 self._maxout = max(1, maxout)
691 self._maxcached = max(0, min(self._maxout, int(maxcached)))
692 else:
693 self._maxout = maxout
694 self._maxcached = max(0, int(maxcached))
695 self._obj = {}
696 if self._FORK_PROTECT:
697 self._pid = _os.getpid()
698 else:
699 self._fork_protect = lambda: None
700
702 """ Destruction """
703 self.shutdown()
704
706 """
707 Create a pooled object
708
709 This method must be implemented by subclasses.
710
711 :return: A new object
712 :rtype: ``PooledInterface``
713 """
714
715
716 raise NotImplementedError()
717
719 """
720 Get a object from the pool
721
722 :return: The new object. If no objects are available in the pool, a
723 new one is created (by calling `_create`). If no object
724 can be created (because of limits), the method blocks.
725 :rtype: `PooledInterface`
726 """
727 self._not_empty.acquire()
728 try:
729 self._fork_protect()
730 while not self._pool:
731 if not self._maxout or len(self._obj) < self._maxout:
732 obj = self._create()
733 try:
734 proxy = _weakref.proxy(obj)
735 except TypeError:
736 proxy = obj
737 self._obj[id(obj)] = proxy
738 break
739 self._not_empty.wait()
740 else:
741 obj = self._pool.pop()
742 self._not_full.notify()
743 return obj
744 finally:
745 self._not_empty.release()
746
748 """
749 Put an object back into the pool
750
751 If the pool is full, the object is destroyed instead. If the object
752 does not come from this pool, it is an error (``assert``).
753
754 :Parameters:
755 - `obj`: The object to put back
756
757 :Types:
758 - `obj`: `PooledInterface`
759 """
760 self._not_full.acquire()
761 try:
762 self._fork_protect()
763 if id(obj) in self._obj:
764 if len(self._pool) >= self._maxcached:
765 obj.destroy()
766 else:
767 self._pool.appendleft(obj)
768 self._not_empty.notify()
769 else:
770 obj.destroy()
771 finally:
772 self._not_full.release()
773
775 """
776 Remove object from pool
777
778 If the object original came not from this pool, this is not an error.
779
780 :Parameters:
781 - `obj`: The object to remove
782
783 :Types:
784 - `obj`: `PooledInterface`
785 """
786 try:
787 del self._obj[id(obj)]
788 except KeyError:
789 pass
790
792 """
793 Clear the currently cached connections
794
795 Connections handed out are not affected. This just empties the cached
796 ones.
797 """
798 self._not_empty.acquire()
799 try:
800 while self._pool:
801 self._pool.pop().destroy()
802 finally:
803 self._not_empty.release()
804
806 """
807 Shutdown this pool
808
809 The queue will be emptied and all objects destroyed. No more
810 objects will be created in this pool. Waiting consumers of the pool
811 will get an ``AssertionError``, because they shouldn't consume anymore
812 anyway.
813 """
814 self._not_full.acquire()
815 try:
816 self._maxcached = 0
817 def create():
818 """ Raise error for new consumers """
819 raise AssertionError("Shutdown in progress")
820 self._create = create
821 self._pool.clear()
822 self._obj, obj = {}, self._obj.values()
823 while obj:
824 obj.pop().destroy()
825 self._not_empty.notifyAll()
826 finally:
827 self._not_full.release()
828
830 """
831 Check if the current PID differs from the stored one
832
833 If they actually differed, we forked and clear the pool. This
834 function should only be called within a locked environment.
835 """
836 pid = _os.getpid()
837 if pid != self._pid:
838 while self._pool:
839 self._pool.pop().destroy()
840 self._obj.clear()
841 self._pid = pid
842
843
845 """
846 Replacement for ``str.__hash__``
847
848 The function is supposed to give identical results on 32 and 64 bit
849 systems.
850
851 :Parameters:
852 - `s`: The string to hash
853
854 :Types:
855 - `s`: ``str``
856
857 :return: The hash value
858 :rtype: ``int``
859 """
860
861
862 raise NotImplementedError()
863
864
866 """
867 Property with improved docs handling
868
869 :Parameters:
870 `func` : ``callable``
871 The function providing the property parameters. It takes no arguments
872 as returns a dict containing the keyword arguments to be defined for
873 ``property``. The documentation is taken out the function by default,
874 but can be overridden in the returned dict.
875
876 :Return: The requested property
877 :Rtype: ``property``
878 """
879 kwargs = func()
880 kwargs.setdefault('doc', func.__doc__)
881 kwargs = kwargs.get
882 return property(
883 fget=kwargs('fget'),
884 fset=kwargs('fset'),
885 fdel=kwargs('fdel'),
886 doc=kwargs('doc'),
887 )
888
889
891 """
892 Determine all public names in space
893
894 :Parameters:
895 `space` : ``dict``
896 Name space to inspect
897
898 :Return: List of public names
899 :Rtype: ``list``
900 """
901 if space.has_key('__all__'):
902 return list(space['__all__'])
903 return [key for key in space.keys() if not key.startswith('_')]
904
905
907 """
908 Represents the package version
909
910 :IVariables:
911 `major` : ``int``
912 The major version number
913
914 `minor` : ``int``
915 The minor version number
916
917 `patch` : ``int``
918 The patch level version number
919
920 `is_dev` : ``bool``
921 Is it a development version?
922
923 `revision` : ``int``
924 SVN Revision
925 """
926
927 - def __new__(cls, versionstring, is_dev, revision):
928 """
929 Construction
930
931 :Parameters:
932 `versionstring` : ``str``
933 The numbered version string (like ``"1.1.0"``)
934 It should contain at least three dot separated numbers
935
936 `is_dev` : ``bool``
937 Is it a development version?
938
939 `revision` : ``int``
940 SVN Revision
941
942 :Return: New version instance
943 :Rtype: `version`
944 """
945
946
947 tup = []
948 versionstring = versionstring.strip()
949 if versionstring:
950 for item in versionstring.split('.'):
951 try:
952 item = int(item)
953 except ValueError:
954 pass
955 tup.append(item)
956 while len(tup) < 3:
957 tup.append(0)
958 return tuple.__new__(cls, tup)
959
960 - def __init__(self, versionstring, is_dev, revision):
961 """
962 Initialization
963
964 :Parameters:
965 `versionstring` : ``str``
966 The numbered version string (like ``1.1.0``)
967 It should contain at least three dot separated numbers
968
969 `is_dev` : ``bool``
970 Is it a development version?
971
972 `revision` : ``int``
973 SVN Revision
974 """
975
976
977 super(Version, self).__init__()
978 self.major, self.minor, self.patch = self[:3]
979 self.is_dev = bool(is_dev)
980 self.revision = int(revision)
981
983 """
984 Create a development string representation
985
986 :Return: The string representation
987 :Rtype: ``str``
988 """
989 return "%s.%s(%r, is_dev=%r, revision=%r)" % (
990 self.__class__.__module__,
991 self.__class__.__name__,
992 ".".join(map(str, self)),
993 self.is_dev,
994 self.revision,
995 )
996
998 """
999 Create a version like string representation
1000
1001 :Return: The string representation
1002 :Rtype: ``str``
1003 """
1004 return "%s%s" % (
1005 ".".join(map(str, self)),
1006 ("", "-dev-r%d" % self.revision)[self.is_dev],
1007 )
1008
1010 """
1011 Create a version like unicode representation
1012
1013 :Return: The unicode representation
1014 :Rtype: ``unicode``
1015 """
1016 return str(self).decode('ascii')
1017
1018
1019 from wtf import c_override
1020 cimpl = c_override('_wtf_cutil')
1021 if cimpl is not None:
1022
1023 hash32 = cimpl.hash32
1024 del c_override, cimpl
1025