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