1 """The TurboGears utility module."""
2
3 _all__ = ['Bunch', 'DictObj', 'DictWrapper', 'Enum', 'setlike',
4 'get_package_name', 'get_model', 'load_project_config',
5 'ensure_sequence', 'has_arg', 'to_kw', 'from_kw', 'adapt_call',
6 'call_on_stack', 'remove_keys', 'arg_index',
7 'inject_arg', 'inject_args', 'add_tg_args', 'bind_args',
8 'recursive_update', 'combine_contexts',
9 'request_available', 'flatten_sequence', 'load_class',
10 'parse_http_accept_header', 'simplify_http_accept_header',
11 'to_unicode', 'to_utf8', 'quote_cookie', 'unquote_cookie',
12 'get_template_encoding_default', 'get_mime_type_for_format',
13 'mime_type_has_charset', 'find_precision', 'copy_if_mutable',
14 'match_ip', 'deprecated']
15
16 import os
17 import sys
18 import re
19 import logging
20 import warnings
21 import htmlentitydefs
22 import socket
23 import struct
24 from inspect import getargspec, getargvalues
25 from itertools import izip, islice, chain
26 from operator import isSequenceType
27 from Cookie import _quote as quote_cookie, _unquote as unquote_cookie
28
29 import pkg_resources
30
31 from cherrypy import request
32
33 from turbogears.decorator import decorator
34 from turbogears import config
38 """Decorator which can be used to mark functions as deprecated.
39
40 It will result in a warning being emitted when the function is used.
41
42 Inspired by http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/391367
43
44 """
45 def decorate(func):
46 if not decorate.message:
47 decorate.message = ("Call to deprecated function %s."
48 % func.__name__)
49 def new_func(*args, **kwargs):
50 if not decorate.warned:
51 warnings.warn(decorate.message, category=DeprecationWarning,
52 stacklevel=2)
53 decorate.warned = True
54 return func(*args, **kwargs)
55 new_func.__name__ = func.__name__
56 new_func.__doc__ = func.__doc__
57 new_func.__dict__.update(func.__dict__)
58 return new_func
59 decorate.message = message
60 decorate.warned = False
61 return decorate
62
65 msg = """\
66 Before you can run this command, you need to install all the project's
67 dependencies by running "python setup.py develop" in the project directory, or
68 you can install the application with "python setup.py install", or build an egg
69 with "python setup.py bdist_egg" and install it with "easy_install dist/<egg>".
70
71 If you are stuck, visit http://docs.turbogears.org/GettingHelp for support."""
72 if name:
73 msg = ("This project requires the %s package but it could not be "
74 "found.\n\n" % name) + msg
75 return msg
76
79 """Simple but handy collector of a bunch of named stuff."""
80
82 keys = self.keys()
83 keys.sort()
84 args = ', '.join(['%s=%r' % (key, self[key]) for key in keys])
85 return '%s(%s)' % (self.__class__.__name__, args)
86
88 try:
89 return self[name]
90 except KeyError:
91 raise AttributeError(name)
92
93 __setattr__ = dict.__setitem__
94
96 try:
97 del self[name]
98 except KeyError:
99 raise AttributeError(name)
100
103
104 @deprecated("Use Bunch instead of DictObj and DictWrapper.")
107
108 DictWrapper = DictObj
109
110
111 -def Enum(*names):
112 """True immutable symbolic enumeration with qualified value access.
113
114 Written by Zoran Isailovski:
115 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/413486
116
117 """
118
119
120
121
122
123 class EnumClass(object):
124 __slots__ = names
125 def __iter__(self):
126 return iter(constants)
127 def __len__(self):
128 return len(constants)
129 def __getitem__(self, i):
130 return constants[i]
131 def __repr__(self):
132 return 'Enum' + str(names)
133 def __str__(self):
134 return 'enum ' + str(constants)
135
136 enumType = EnumClass()
137
138 class EnumValue(object):
139 __slots__ = ('__value')
140 def __init__(self, value):
141 self.__value = value
142 Value = property(lambda self: self.__value)
143 EnumType = property(lambda self: enumType)
144 def __hash__(self):
145 return hash(self.__value)
146 def __cmp__(self, other):
147
148
149 if not (isinstance(other, EnumValue)
150 and self.EnumType is other.EnumType):
151 raise TypeError("Only values from the same enum are comparable")
152 return cmp(self.__value, other.__value)
153 def __invert__(self):
154 return constants[maximum - self.__value]
155 def __nonzero__(self):
156 return bool(self.__value)
157 def __repr__(self):
158 return str(names[self.__value])
159
160 maximum = len(names) - 1
161 constants = [None] * len(names)
162 for i, each in enumerate(names):
163 val = EnumValue(i)
164 setattr(EnumClass, each, val)
165 constants[i] = val
166 constants = tuple(constants)
167 return enumType
168
171 """Set preserving item order."""
172
173 - def add(self, item):
174 if item not in self:
175 self.append(item)
176
178 for item in iterable:
179 self.add(item)
180
188
191 """Try to select appropriate project configuration file."""
192 return os.path.exists('setup.py') and 'dev.cfg' or 'prod.cfg'
193
196 """Try to update the project settings from the config file specified.
197
198 If configfile is C{None}, uses L{get_project_config} to locate one.
199
200 """
201 if configfile is None:
202 configfile = get_project_config()
203 if not os.path.isfile(configfile):
204 print 'Config file %s not found or is not a file.' % (
205 os.path.abspath(configfile),)
206 sys.exit()
207 package = get_package_name()
208 config.update_config(configfile=configfile, modulename=package + '.config')
209
212 """Try to find out the package name of the current directory."""
213 package = config.get('package')
214 if package:
215 return package
216 if hasattr(sys, 'argv') and "--egg" in sys.argv:
217 projectname = sys.argv[sys.argv.index("--egg")+1]
218 egg = pkg_resources.get_distribution(projectname)
219 top_level = egg._get_metadata("top_level.txt")
220 else:
221 fname = get_project_meta('top_level.txt')
222 top_level = fname and open(fname) or []
223 for package in top_level:
224 package = package.rstrip()
225 if package and package != 'locales':
226 return package
227
234
243
246 """Construct a sequence from object."""
247 if obj is None:
248 return []
249 elif isSequenceType(obj):
250 return obj
251 else:
252 return [obj]
253
254
255 -def to_kw(func, args, kw, start=0):
256 """Convert all applicable arguments to keyword arguments."""
257 argnames, defaults = getargspec(func)[::3]
258 defaults = ensure_sequence(defaults)
259 kv_pairs = izip(
260 islice(argnames, start, len(argnames) - len(defaults)), args)
261 for k, v in kv_pairs:
262 kw[k] = v
263 return args[len(argnames)-len(defaults)-start:], kw
264
265
266 -def from_kw(func, args, kw, start=0):
267 """Extract named positional arguments from keyword arguments."""
268 argnames, defaults = getargspec(func)[::3]
269 defaults = ensure_sequence(defaults)
270 newargs = [kw.pop(name) for name in islice(argnames, start,
271 len(argnames) - len(defaults)) if name in kw]
272 newargs.extend(args)
273 return newargs, kw
274
277 """Remove unsupported func arguments from given args list and kw dict.
278
279 @param func: the callable to inspect for supported arguments
280 @type func: callable
281
282 @param args: the names of the positional arguments intended to be passed
283 to func
284 @type args: list
285
286 @param kw: the keyword arguments intended to be passed to func
287 @type kw: dict
288
289 @keyparam start: the number of items from the start of the argument list of
290 func to disregard. Set start=1 to use adapt_call on a bound method to
291 disregard the implicit self argument.
292
293 @type start: int
294
295 Returns args list and kw dict from which arguments unsupported by func
296 have been removed. The passed in kw dict is also stripped as a side-effect.
297 The returned objects can then be used to call the target function.
298
299 Example:
300
301 def myfunc(arg1, arg2, kwarg1='foo'):
302 pass
303
304 args, kw = adapt_call(myfunc, ['args1, 'bogus1'],
305 {'kwargs1': 'bar', 'bogus2': 'spamm'})
306 # --> ['args1'], {'kwargs1': 'bar'}
307 myfunc(*args, **kw)
308
309 """
310 argnames, varargs, kwargs = getargspec(func)[:3]
311 del argnames[:start]
312 if kwargs in (None, "_decorator__kwargs"):
313 remove_keys(kw, [key for key in kw if key not in argnames])
314 if varargs in (None, "_decorator__varargs"):
315 args = args[:len(argnames)]
316 for n, key in enumerate(argnames):
317 if key in kw:
318 args = args[:n]
319 break
320 return args, kw
321
324 """Check if a call to function matching pattern is on stack."""
325 try:
326 frame = sys._getframe(start+1)
327 except ValueError:
328 return False
329 while frame.f_back:
330 frame = frame.f_back
331 if frame.f_code.co_name == func_name:
332 args = getargvalues(frame)[3]
333 for key in kw.iterkeys():
334 try:
335 if kw[key] != args[key]:
336 break
337 except (KeyError, TypeError):
338 break
339 else:
340 return True
341 return False
342
345 """Gracefully remove keys from dict."""
346 for key in seq:
347 dict_.pop(key, None)
348 return dict_
349
352 """Check whether function has argument."""
353 return argname in getargspec(func)[0]
354
357 """Find index of argument as declared for given function."""
358 argnames = getargspec(func)[0]
359 if has_arg(func, argname):
360 return argnames.index(argname)
361 else:
362 return None
363
364
365 -def inject_arg(func, argname, argval, args, kw, start=0):
366 """Insert argument into call."""
367 argnames, defaults = getargspec(func)[::3]
368 defaults = ensure_sequence(defaults)
369 pos = arg_index(func, argname)
370 if pos is None or pos > len(argnames) - len(defaults) - 1:
371 kw[argname] = argval
372 else:
373 pos -= start
374 args = tuple(chain(islice(args, pos), (argval,),
375 islice(args, pos, None)))
376 return args, kw
377
378
379 -def inject_args(func, injections, args, kw, start=0):
380 """Insert arguments into call."""
381 for argname, argval in injections.iteritems():
382 args, kw = inject_arg(func, argname, argval, args, kw, start)
383 return args, kw
384
387 """Insert arguments and call."""
388 args, kw = inject_args(func, injections, args, kw)
389 return func(*args, **kw)
390
393 """Add hint for special arguments that shall not be removed."""
394 try:
395 tg_args = func._tg_args
396 except AttributeError:
397 tg_args = set()
398 tg_args.update(args)
399 func._tg_args = tg_args
400
403 """Call with arguments set to a predefined value."""
404 def entagle(func):
405 return lambda func, *args, **kw: inject_call(func, add, *args, **kw)
406
407 def make_decorator(func):
408 argnames, varargs, kwargs, defaults = getargspec(func)
409 defaults = list(ensure_sequence(defaults))
410 defaults = [d for d in defaults if
411 argnames[-len(defaults) + defaults.index(d)] not in add]
412 argnames = [arg for arg in argnames if arg not in add]
413 return decorator(entagle, (argnames, varargs, kwargs, defaults))(func)
414
415 return make_decorator
416
419 """Recursively update all dicts in to_dict with values from from_dict."""
420
421 for k, v in from_dict.iteritems():
422 if isinstance(v, dict) and isinstance(to_dict[k], dict):
423 recursive_update(to_dict[k], v)
424 else:
425 to_dict[k] = v
426 return to_dict
427
428
429 -def combine_contexts(frames=None, depth=None):
430 """Combine contexts (globals, locals) of frames."""
431 locals_ = {}
432 globals_ = {}
433 if frames is None:
434 frames = []
435 if depth is not None:
436 frames.extend([sys._getframe(d+1) for d in depth])
437 for frame in frames:
438 locals_.update(frame.f_locals)
439 globals_.update(frame.f_globals)
440 return locals_, globals_
441
444 """Check if cherrypy.request is available."""
445 stage = getattr(request, 'stage', None)
446 return stage is not None
447
450 """Flatten sequence."""
451 for item in seq:
452 if isSequenceType(item) and not isinstance(item, basestring):
453 for item in flatten_sequence(item):
454 yield item
455 else:
456 yield item
457
460 """Load a class from a module in dotted-path notation.
461
462 E.g.: load_class("package.module.class").
463
464 Based on recipe 16.3 from Python Cookbook, 2ed., by Alex Martelli,
465 Anna Martelli Ravenscroft, and David Ascher (O'Reilly Media, 2005)
466
467 """
468 assert dottedpath is not None, "dottedpath must not be None"
469 splitted_path = dottedpath.split('.')
470 modulename = '.'.join(splitted_path[:-1])
471 classname = splitted_path[-1]
472 try:
473 try:
474 module = __import__(modulename, globals(), locals(), [classname])
475 except ValueError:
476 if not modulename:
477 module = __import__(__name__.split('.', 1)[0],
478 globals(), locals(), [classname])
479 except ImportError:
480
481
482 logging.exception('tg.utils: Could not import %s'
483 ' because an exception occurred', dottedpath)
484 return None
485 try:
486 return getattr(module, classname)
487 except AttributeError:
488 logging.exception('tg.utils: Could not import %s'
489 ' because the class was not found', dottedpath)
490 return None
491
494 """Parse an HTTP Accept header (RFC 2616) into a sorted list.
495
496 The quality factors in the header determine the sort order.
497 The values can include possible media-range parameters.
498 This function can also be used for the Accept-Charset,
499 Accept-Encoding and Accept-Language headers.
500
501 """
502 if accept is None:
503 return []
504 items = []
505 for item in accept.split(','):
506 params = item.split(';')
507 for i, param in enumerate(params[1:]):
508 param = param.split('=', 1)
509 if param[0].strip() == 'q':
510 try:
511 q = float(param[1])
512 if not 0 < q <= 1:
513 raise ValueError
514 except (IndexError, ValueError):
515 q = 0
516 else:
517 item = ';'.join(params[:i+1])
518 break
519 else:
520 q = 1
521 if q:
522 item = item.strip()
523 if item:
524 items.append((item, q))
525 items.sort(key=lambda item: -item[1])
526 return [item[0] for item in items]
527
530 """Parse an HTTP Accept header (RFC 2616) into a preferred value.
531
532 The quality factors in the header determine the preference.
533 Possible media-range parameters are allowed, but will be ignored.
534 This function can also be used for the Accept-Charset,
535 Accept-Encoding and Accept-Language headers.
536
537 This is similar to parse_http_accept_header(accept)[0], but faster.
538
539 """
540 if accept is None:
541 return None
542 best_item = accept
543 best_q = 0
544 for item in accept.split(','):
545 params = item.split(';')
546 item = params.pop(0)
547 for param in params:
548 param = param.split('=', 1)
549 if param[0].strip() == 'q':
550 try:
551 q = float(param[1])
552 if not 0 < q <= 1:
553 raise ValueError
554 except (IndexError, ValueError):
555 q = 0
556 break
557 else:
558 q = 1
559 if q > best_q:
560 item = item.strip()
561 if item:
562 best_item = item
563 if q == 1:
564 break
565 best_q = q
566 return best_item
567
570 """Convert encoded string to unicode string.
571
572 Uses get_template_encoding_default() to guess source string encoding.
573 Handles turbogears.i18n.lazystring correctly.
574
575 """
576 if isinstance(value, str):
577
578
579 try:
580 value = unicode(value)
581 except UnicodeDecodeError:
582 try:
583 value = unicode(value, get_template_encoding_default())
584 except UnicodeDecodeError:
585
586 raise ValueError("Non-unicode string: %r" % value)
587 return value
588
591 """Convert a unicode string to utf-8 encoded plain string.
592
593 Handles turbogears.i18n.lazystring correctly.
594
595 Does nothing to already encoded string.
596
597 """
598 if isinstance(value, str):
599 pass
600 elif hasattr(value, '__unicode__'):
601 value = unicode(value)
602 if isinstance(value, unicode):
603 value = value.encode('utf-8')
604 return value
605
613
614
615 _format_mime_types = dict(
616 plain='text/plain', text='text/plain',
617 html='text/html', xhtml = 'text/html',
618 xml='text/xml', json='application/json')
643
646 """Return whether the MIME media type supports a charset parameter.
647
648 Note: According to RFC4627, we do not output a charset parameter
649 for "application/json" (this type always uses a UTF encoding).
650
651 """
652 if not mime_type:
653 return False
654 if mime_type.startswith('text/'):
655 return True
656 if mime_type.startswith('application/'):
657 if mime_type.endswith('/xml') or mime_type.endswith('+xml'):
658 return True
659 if mime_type.endswith('/javascript'):
660 return True
661 return False
662
665 """Find precision of some arbitrary value.
666
667 The main intention for this function is to use it together with
668 turbogears.i18n.format.format_decimal() where one has to inform
669 the precision wanted. So, use it like this:
670
671 format_decimal(some_number, find_precision(some_number))
672
673 """
674 decimals = ''
675 try:
676 decimals = str(value).split('.', 1)[1]
677 except IndexError:
678 pass
679 return len(decimals)
680
683 """Make a copy of the value if it is mutable.
684
685 Returns the value. If feedback is set to true, also returns
686 whether value was mutable as the second element of a tuple.
687
688 """
689 if isinstance(value, dict):
690 mutable = True
691 value = value.copy()
692 elif isinstance(value, list):
693 mutable = True
694 value = value[:]
695 else:
696 mutable = False
697 if feedback:
698 return value, mutable
699 else:
700 return value
701
704 """Replace HTML character entities with numerical references.
705
706 Note: This won't handle CDATA sections properly.
707
708 """
709 def repl(matchobj):
710 entity = htmlentitydefs.entitydefs.get(matchobj.group(1).lower())
711 if not entity:
712 return matchobj.group(0)
713 elif len(entity) == 1:
714 if entity in '&<>\'"':
715 return matchobj.group(0)
716 return '&#%d;' % ord(entity)
717 else:
718 return entity
719 return re.sub('&(\w+);?', repl, htmltext)
720
721
722 if hasattr(socket, 'inet_pton') and hasattr(socket, 'AF_INET6'):
725 """Convert IP6 standard hex notation to IP6 address."""
726 return socket.inet_pton(socket.AF_INET6, addr)
727
728 else:
729
730 import string
731 _inet6_chars = string.hexdigits + ':.'
734 """Convert IPv6 standard hex notation to IPv6 address.
735
736 Inspired by http://twistedmatrix.com/trac/.
737
738 """
739 faulty = addr.lstrip(_inet6_chars)
740 if faulty:
741 raise ValueError("Illegal character '%c' in IPv6 address" % faulty[0])
742 parts = addr.split(':')
743 elided = parts.count('')
744 extenso = '.' in parts[-1] and 7 or 8
745 if len(parts) > extenso or elided > 3:
746 raise ValueError("Syntactically invalid IPv6 address")
747 if elided == 3:
748 return '\x00' * 16
749 if elided:
750 zeros = ['0'] * (extenso - len(parts) + elided)
751 if addr.startswith('::'):
752 parts[:2] = zeros
753 elif addr.endswith('::'):
754 parts[-2:] = zeros
755 else:
756 idx = parts.index('')
757 parts[idx:idx+1] = zeros
758 if len(parts) != extenso:
759 raise ValueError("Syntactically invalid IPv6 address")
760 if extenso == 7:
761 ipv4 = parts.pop()
762 if ipv4.count('.') != 3:
763 raise ValueError("Syntactically invalid IPv6 address")
764 parts = [int(x, 16) for x in parts]
765 return struct.pack('!6H', *parts) + socket.inet_aton(ipv4)
766 else:
767 parts = [int(x, 16) for x in parts]
768 return struct.pack('!8H', *parts)
769
772 """Convert IPv4 or IPv6 notation to IPv6 address."""
773 if ':' in addr:
774 return inet6_aton(addr)
775 else:
776 return struct.pack('!QL', 0, 0xffff) + socket.inet_aton(addr)
777
780 """Remove the number of masked bits from the IPV6 address."""
781 hi, lo = struct.unpack("!QQ", addr)
782 return (hi << 64 | lo) >> masked
783
786 """Check whether IP address matches CIDR IP address block."""
787 if '/' in cidr:
788 cidr, prefix = cidr.split('/', 1)
789 masked = (':' in cidr and 128 or 32) - int(prefix)
790 else:
791 masked = None
792 cidr = inet_aton(cidr)
793 ip = inet_aton(ip)
794 if masked:
795 cidr = _inet_prefix(cidr, masked)
796 ip = _inet_prefix(ip, masked)
797 return ip == cidr
798