Steve Piercy
2018-09-22 2a45fe74f9598b4e726ab17ce17948d4e709894b
commit | author | age
19016b 1 import contextlib
d8d3a9 2 import functools
716a20 3 try:
MM 4     # py2.7.7+ and py3.3+ have native comparison support
5     from hmac import compare_digest
af01a5 6 except ImportError:  # pragma: no cover
716a20 7     compare_digest = None
8b6f09 8 import inspect
d8d3a9 9 import traceback
91cd7e 10 import weakref
d8d3a9 11
CM 12 from zope.interface import implementer
a9f17c 13
66fe1d 14 from pyramid.exceptions import (
CM 15     ConfigurationError,
16     CyclicDependencyError,
17     )
18
8a4c36 19 from pyramid.compat import (
7dd976 20     getargspec,
MM 21     im_func,
66fe1d 22     is_nonstr_iter,
8a4c36 23     integer_types,
CM 24     string_types,
e49638 25     text_,
bc37a5 26     PY2,
1e0d64 27     native_
8a4c36 28     )
CM 29
d8d3a9 30 from pyramid.interfaces import IActionInfo
078859 31 from pyramid.path import DottedNameResolver as _DottedNameResolver
a9f17c 32
c7974f 33 _marker = object()
MM 34
50a8a0 35
078859 36 class DottedNameResolver(_DottedNameResolver):
CM 37     def __init__(self, package=None): # default to package = None for bw compat
0f2a11 38         _DottedNameResolver.__init__(self, package)
a9f17c 39
c7974f 40 def is_string_or_iterable(v):
MM 41     if isinstance(v, string_types):
42         return True
43     if hasattr(v, '__iter__'):
44         return True
f58977 45
c7974f 46 def as_sorted_tuple(val):
MM 47     if not is_nonstr_iter(val):
48         val = (val,)
49     val = tuple(sorted(val))
50     return val
a9f17c 51
04cc91 52 class InstancePropertyHelper(object):
MM 53     """A helper object for assigning properties and descriptors to instances.
54     It is not normally possible to do this because descriptors must be
55     defined on the class itself.
56
57     This class is optimized for adding multiple properties at once to an
58     instance. This is done by calling :meth:`.add_property` once
59     per-property and then invoking :meth:`.apply` on target objects.
60
577db9 61     """
04cc91 62     def __init__(self):
MM 63         self.properties = {}
577db9 64
6d2187 65     @classmethod
04cc91 66     def make_property(cls, callable, name=None, reify=False):
5473f0 67         """ Convert a callable into one suitable for adding to the
MM 68         instance. This will return a 2-tuple containing the computed
69         (name, property) pair.
70         """
71
72         is_property = isinstance(callable, property)
73         if is_property:
74             fn = callable
75             if name is None:
76                 raise ValueError('must specify "name" for a property')
77             if reify:
78                 raise ValueError('cannot reify a property')
79         elif name is not None:
80             fn = lambda this: callable(this)
1e0d64 81             fn.__name__ = get_callable_name(name)
5473f0 82             fn.__doc__ = callable.__doc__
MM 83         else:
84             name = callable.__name__
85             fn = callable
86         if reify:
87             import pyramid.decorator # avoid circular import
88             fn = pyramid.decorator.reify(fn)
89         elif not is_property:
90             fn = property(fn)
91
92         return name, fn
93
04cc91 94     @classmethod
MM 95     def apply_properties(cls, target, properties):
2f0ba0 96         """Accept a list or dict of ``properties`` generated from
MM 97         :meth:`.make_property` and apply them to a ``target`` object.
5473f0 98         """
MM 99         attrs = dict(properties)
0cd88b 100         if attrs:
04cc91 101             parent = target.__class__
MM 102             newcls = type(parent.__name__, (parent, object), attrs)
c600ab 103             # We assign __provides__ and __implemented__ below to prevent a
BJR 104             # memory leak that results from from the usage of this instance's
105             # eventual use in an adapter lookup.  Adapter lookup results in
106             # ``zope.interface.implementedBy`` being called with the
f58977 107             # newly-created class as an argument.  Because the newly-created
CM 108             # class has no interface specification data of its own, lookup
109             # causes new ClassProvides and Implements instances related to our
110             # just-generated class to be created and set into the newly-created
111             # class' __dict__.  We don't want these instances to be created; we
112             # want this new class to behave exactly like it is the parent class
32e969 113             # instead.  See GitHub issues #1212, #1529 and #1568 for more
CM 114             # information.
c600ab 115             for name in ('__implemented__', '__provides__'):
f58977 116                 # we assign these attributes conditionally to make it possible
CM 117                 # to test this class in isolation without having any interfaces
118                 # attached to it
119                 val = getattr(parent, name, _marker)
120                 if val is not _marker:
04cc91 121                     setattr(newcls, name, val)
MM 122             target.__class__ = newcls
5473f0 123
04cc91 124     @classmethod
MM 125     def set_property(cls, target, callable, name=None, reify=False):
126         """A helper method to apply a single property to an instance."""
127         prop = cls.make_property(callable, name=name, reify=reify)
128         cls.apply_properties(target, [prop])
9e7248 129
04cc91 130     def add_property(self, callable, name=None, reify=False):
MM 131         """Add a new property configuration.
132
133         This should be used in combination with :meth:`.apply` as a
134         more efficient version of :meth:`.set_property`.
135         """
136         name, fn = self.make_property(callable, name=name, reify=reify)
137         self.properties[name] = fn
138
139     def apply(self, target):
140         """ Apply all configured properties to the ``target`` instance."""
141         if self.properties:
142             self.apply_properties(target, self.properties)
143
144 class InstancePropertyMixin(object):
145     """ Mixin that will allow an instance to add properties at
146     run-time as if they had been defined via @property or @reify
147     on the class itself.
148     """
735987 149
1b113f 150     def set_property(self, callable, name=None, reify=False):
577db9 151         """ Add a callable or a property descriptor to the instance.
MM 152
153         Properties, unlike attributes, are lazily evaluated by executing
154         an underlying callable when accessed. They can be useful for
155         adding features to an object without any cost if those features
156         go unused.
157
158         A property may also be reified via the
159         :class:`pyramid.decorator.reify` decorator by setting
160         ``reify=True``, allowing the result of the evaluation to be
5473f0 161         cached. Using this method, the value of the property is only
MM 162         computed once for the lifetime of the object.
577db9 163
1b113f 164         ``callable`` can either be a callable that accepts the instance
5473f0 165         as its single positional parameter, or it can be a property
577db9 166         descriptor.
MM 167
1b113f 168         If the ``callable`` is a property descriptor, the ``name``
MM 169         parameter must be supplied or a ``ValueError`` will be raised.
170         Also note that a property descriptor cannot be reified, so
171         ``reify`` must be ``False``.
577db9 172
MM 173         If ``name`` is None, the name of the property will be computed
1b113f 174         from the name of the ``callable``.
577db9 175
MM 176         .. code-block:: python
177            :linenos:
178
179            class Foo(InstancePropertyMixin):
180                _x = 1
181
182            def _get_x(self):
183                return _x
184
185            def _set_x(self, value):
186                self._x = value
187
188            foo = Foo()
189            foo.set_property(property(_get_x, _set_x), name='x')
190            foo.set_property(_get_x, name='y', reify=True)
191
192            >>> foo.x
193            1
194            >>> foo.y
195            1
196            >>> foo.x = 5
197            >>> foo.x
198            5
199            >>> foo.y # notice y keeps the original value
200            1
201         """
04cc91 202         InstancePropertyHelper.set_property(
MM 203             self, callable, name=name, reify=reify)
577db9 204
91cd7e 205 class WeakOrderedSet(object):
MM 206     """ Maintain a set of items.
a9f17c 207
91cd7e 208     Each item is stored as a weakref to avoid extending their lifetime.
MM 209
210     The values may be iterated over or the last item added may be
211     accessed via the ``last`` property.
01c4f3 212
MM 213     If items are added more than once, the most recent addition will
214     be remembered in the order:
215
216         order = WeakOrderedSet()
217         order.add('1')
218         order.add('2')
219         order.add('1')
220
221         list(order) == ['2', '1']
222         order.last == '1'
91cd7e 223     """
MM 224
225     def __init__(self):
226         self._items = {}
227         self._order = []
228
229     def add(self, item):
eff1cb 230         """ Add an item to the set."""
91cd7e 231         oid = id(item)
MM 232         if oid in self._items:
01c4f3 233             self._order.remove(oid)
MM 234             self._order.append(oid)
91cd7e 235             return
0393f9 236         ref = weakref.ref(item, lambda x: self._remove_by_id(oid))
91cd7e 237         self._items[oid] = ref
MM 238         self._order.append(oid)
239
0393f9 240     def _remove_by_id(self, oid):
eff1cb 241         """ Remove an item from the set."""
MM 242         if oid in self._items:
243             del self._items[oid]
244             self._order.remove(oid)
245
0393f9 246     def remove(self, item):
KK 247         """ Remove an item from the set."""
248         self._remove_by_id(id(item))
249
eff1cb 250     def empty(self):
MM 251         """ Clear all objects from the set."""
252         self._items = {}
253         self._order = []
254
91cd7e 255     def __len__(self):
MM 256         return len(self._order)
257
258     def __contains__(self, item):
259         oid = id(item)
260         return oid in self._items
261
262     def __iter__(self):
263         return (self._items[oid]() for oid in self._order)
264
265     @property
266     def last(self):
267         if self._order:
268             oid = self._order[-1]
269             return self._items[oid]()
654a67 270
716a20 271 def strings_differ(string1, string2, compare_digest=compare_digest):
13906d 272     """Check whether two strings differ while avoiding timing attacks.
RK 273
274     This function returns True if the given strings differ and False
275     if they are equal.  It's careful not to leak information about *where*
276     they differ as a result of its running time, which can be very important
277     to avoid certain timing-related crypto attacks:
278
279         http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf
280
716a20 281     .. versionchanged:: 1.6
MM 282        Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+
283        and Python 3.3+).
284
13906d 285     """
716a20 286     len_eq = len(string1) == len(string2)
MM 287     if len_eq:
288         invalid_bits = 0
289         left = string1
290     else:
291         invalid_bits = 1
292         left = string2
293     right = string2
13906d 294
716a20 295     if compare_digest is not None:
MM 296         invalid_bits += not compare_digest(left, right)
297     else:
298         for a, b in zip(left, right):
299             invalid_bits += a != b
13906d 300     return invalid_bits != 0
RK 301
8b6f09 302 def object_description(object):
e49638 303     """ Produce a human-consumable text description of ``object``,
CM 304     usually involving a Python dotted name. For example:
8b6f09 305
88cafd 306     >>> object_description(None)
TL 307     u'None'
308     >>> from xml.dom import minidom
309     >>> object_description(minidom)
310     u'module xml.dom.minidom'
311     >>> object_description(minidom.Attr)
312     u'class xml.dom.minidom.Attr'
313     >>> object_description(minidom.Attr.appendChild)
314     u'method appendChild of class xml.dom.minidom.Attr'
8b6f09 315
CM 316     If this method cannot identify the type of the object, a generic
317     description ala ``object <object.__name__>`` will be returned.
318
319     If the object passed is already a string, it is simply returned.  If it
320     is a boolean, an integer, a list, a tuple, a set, or ``None``, a
321     (possibly shortened) string representation is returned.
322     """
323     if isinstance(object, string_types):
e49638 324         return text_(object)
8a4c36 325     if isinstance(object, integer_types):
e49638 326         return text_(str(object))
8a4c36 327     if isinstance(object, (bool, float, type(None))):
e49638 328         return text_(str(object))
82ba10 329     if isinstance(object, set):
bc37a5 330         if PY2:
82ba10 331             return shortrepr(object, ')')
bc37a5 332         else:
MM 333             return shortrepr(object, '}')
82ba10 334     if isinstance(object, tuple):
8b6f09 335         return shortrepr(object, ')')
CM 336     if isinstance(object, list):
337         return shortrepr(object, ']')
338     if isinstance(object, dict):
339         return shortrepr(object, '}')
340     module = inspect.getmodule(object)
7f72f8 341     if module is None:
e49638 342         return text_('object %s' % str(object))
8b6f09 343     modulename = module.__name__
CM 344     if inspect.ismodule(object):
e49638 345         return text_('module %s' % modulename)
8b6f09 346     if inspect.ismethod(object):
CM 347         oself = getattr(object, '__self__', None)
e49638 348         if oself is None: # pragma: no cover
8b6f09 349             oself = getattr(object, 'im_self', None)
e49638 350         return text_('method %s of class %s.%s' %
CM 351                      (object.__name__, modulename,
352                       oself.__class__.__name__))
1b113f 353
8b6f09 354     if inspect.isclass(object):
CM 355         dottedname = '%s.%s' % (modulename, object.__name__)
e49638 356         return text_('class %s' % dottedname)
8b6f09 357     if inspect.isfunction(object):
CM 358         dottedname = '%s.%s' % (modulename, object.__name__)
e49638 359         return text_('function %s' % dottedname)
CM 360     return text_('object %s' % str(object))
8b6f09 361
CM 362 def shortrepr(object, closer):
363     r = str(object)
364     if len(r) > 100:
e49638 365         r = r[:100] + ' ... %s' % closer
8b6f09 366     return r
d98612 367
66fe1d 368 class Sentinel(object):
CM 369     def __init__(self, repr):
370         self.repr = repr
371
372     def __repr__(self):
373         return self.repr
374
375 FIRST = Sentinel('FIRST')
376 LAST = Sentinel('LAST')
377
378 class TopologicalSorter(object):
379     """ A utility class which can be used to perform topological sorts against
380     tuple-like data."""
381     def __init__(
382         self,
383         default_before=LAST,
384         default_after=None,
385         first=FIRST,
386         last=LAST,
387         ):
388         self.names = []
389         self.req_before = set()
390         self.req_after = set()
391         self.name2before = {}
392         self.name2after = {}
393         self.name2val = {}
394         self.order = []
395         self.default_before = default_before
396         self.default_after = default_after
397         self.first = first
398         self.last = last
399
e4b931 400     def values(self):
MM 401         return self.name2val.values()
402
66fe1d 403     def remove(self, name):
CM 404         """ Remove a node from the sort input """
405         self.names.remove(name)
406         del self.name2val[name]
407         after = self.name2after.pop(name, [])
408         if after:
409             self.req_after.remove(name)
410             for u in after:
411                 self.order.remove((u, name))
412         before = self.name2before.pop(name, [])
413         if before:
414             self.req_before.remove(name)
415             for u in before:
416                 self.order.remove((name, u))
417                 
418     def add(self, name, val, after=None, before=None):
419         """ Add a node to the sort input.  The ``name`` should be a string or
420         any other hashable object, the ``val`` should be the sortable (doesn't
421         need to be hashable).  ``after`` and ``before`` represents the name of
422         one of the other sortables (or a sequence of such named) or one of the
423         special sentinel values :attr:`pyramid.util.FIRST`` or
424         :attr:`pyramid.util.LAST` representing the first or last positions
425         respectively.  ``FIRST`` and ``LAST`` can also be part of a sequence
426         passed as ``before`` or ``after``.  A sortable should not be added
427         after LAST or before FIRST.  An example::
428
429            sorter = TopologicalSorter()
430            sorter.add('a', {'a':1}, before=LAST, after='b')
431            sorter.add('b', {'b':2}, before=LAST, after='c')
432            sorter.add('c', {'c':3})
433
434            sorter.sorted() # will be {'c':3}, {'b':2}, {'a':1}
435
436         """
437         if name in self.names:
438             self.remove(name)
439         self.names.append(name)
440         self.name2val[name] = val
441         if after is None and before is None:
442             before = self.default_before
443             after = self.default_after
444         if after is not None:
445             if not is_nonstr_iter(after):
446                 after = (after,)
447             self.name2after[name] = after
448             self.order += [(u, name) for u in after]
449             self.req_after.add(name)
450         if before is not None:
451             if not is_nonstr_iter(before):
452                 before = (before,)
453             self.name2before[name] = before
454             self.order += [(name, o) for o in before]
455             self.req_before.add(name)
456
457
458     def sorted(self):
459         """ Returns the sort input values in topologically sorted order"""
460         order = [(self.first, self.last)]
461         roots = []
462         graph = {}
463         names = [self.first, self.last]
464         names.extend(self.names)
465
466         for a, b in self.order:
467             order.append((a, b))
468
469         def add_node(node):
25c64c 470             if node not in graph:
66fe1d 471                 roots.append(node)
CM 472                 graph[node] = [0] # 0 = number of arcs coming into this node
473
474         def add_arc(fromnode, tonode):
475             graph[fromnode].append(tonode)
476             graph[tonode][0] += 1
477             if tonode in roots:
478                 roots.remove(tonode)
479
480         for name in names:
481             add_node(name)
482
483         has_before, has_after = set(), set()
484         for a, b in order:
485             if a in names and b in names: # deal with missing dependencies
486                 add_arc(a, b)
487                 has_before.add(a)
488                 has_after.add(b)
489
490         if not self.req_before.issubset(has_before):
491             raise ConfigurationError(
492                 'Unsatisfied before dependencies: %s'
493                 % (', '.join(sorted(self.req_before - has_before)))
494             )
495         if not self.req_after.issubset(has_after):
496             raise ConfigurationError(
497                 'Unsatisfied after dependencies: %s'
498                 % (', '.join(sorted(self.req_after - has_after)))
499             )
500
501         sorted_names = []
502
503         while roots:
504             root = roots.pop(0)
505             sorted_names.append(root)
506             children = graph[root][1:]
507             for child in children:
508                 arcs = graph[child][0]
509                 arcs -= 1
510                 graph[child][0] = arcs 
511                 if arcs == 0:
512                     roots.insert(0, child)
513             del graph[root]
514
515         if graph:
516             # loop in input
517             cycledeps = {}
518             for k, v in graph.items():
519                 cycledeps[k] = v[1:]
520             raise CyclicDependencyError(cycledeps)
521
522         result = []
523
524         for name in sorted_names:
525             if name in self.names:
526                 result.append((name, self.name2val[name]))
527
528         return result
529
d8d3a9 530 def viewdefaults(wrapped):
CM 531     """ Decorator for add_view-like methods which takes into account
532     __view_defaults__ attached to view it is passed.  Not a documented API but
533     used by some external systems."""
534     def wrapper(self, *arg, **kw):
535         defaults = {}
536         if arg:
537             view = arg[0]
538         else:
539             view = kw.get('view')
540         view = self.maybe_dotted(view)
541         if inspect.isclass(view):
542             defaults = getattr(view, '__view_defaults__', {}).copy()
25c64c 543         if '_backframes' not in kw:
0920ac 544             kw['_backframes'] = 1 # for action_method
d8d3a9 545         defaults.update(kw)
CM 546         return wrapped(self, *arg, **defaults)
547     return functools.wraps(wrapped)(wrapper)
548
549 @implementer(IActionInfo)
550 class ActionInfo(object):
551     def __init__(self, file, line, function, src):
552         self.file = file
553         self.line = line
554         self.function = function
555         self.src = src
556
557     def __str__(self):
558         srclines = self.src.split('\n')
559         src = '\n'.join('    %s' % x for x in srclines)
560         return 'Line %s of file %s:\n%s' % (self.line, self.file, src)
561
562 def action_method(wrapped):
563     """ Wrapper to provide the right conflict info report data when a method
564     that calls Configurator.action calls another that does the same.  Not a
565     documented API but used by some external systems."""
566     def wrapper(self, *arg, **kw):
567         if self._ainfo is None:
568             self._ainfo = []
569         info = kw.pop('_info', None)
570         # backframes for outer decorators to actionmethods
0920ac 571         backframes = kw.pop('_backframes', 0) + 2
d8d3a9 572         if is_nonstr_iter(info) and len(info) == 4:
CM 573             # _info permitted as extract_stack tuple
574             info = ActionInfo(*info)
575         if info is None:
576             try:
ce042d 577                 f = traceback.extract_stack(limit=4)
BJR 578
579                 # Work around a Python 3.5 issue whereby it would insert an
580                 # extra stack frame. This should no longer be necessary in
581                 # Python 3.5.1
582                 last_frame = ActionInfo(*f[-1])
601393 583                 if last_frame.function == 'extract_stack': # pragma: no cover
ce042d 584                     f.pop()
d8d3a9 585                 info = ActionInfo(*f[-backframes])
5705e1 586             except Exception: # pragma: no cover
d8d3a9 587                 info = ActionInfo(None, 0, '', '')
CM 588         self._ainfo.append(info)
589         try:
590             result = wrapped(self, *arg, **kw)
591         finally:
592             self._ainfo.pop()
593         return result
594
595     if hasattr(wrapped, '__name__'):
596         functools.update_wrapper(wrapper, wrapped)
597     wrapper.__docobj__ = wrapped
598     return wrapper
1e0d64 599
JA 600
601 def get_callable_name(name):
602     """
603     Verifies that the ``name`` is ascii and will raise a ``ConfigurationError``
604     if it is not.
605     """
606     try:
607         return native_(name, 'ascii')
608     except (UnicodeEncodeError, UnicodeDecodeError):
609         msg = (
610             '`name="%s"` is invalid. `name` must be ascii because it is '
611             'used on __name__ of the method'
612         )
613         raise ConfigurationError(msg % name)
19016b 614
MM 615 @contextlib.contextmanager
616 def hide_attrs(obj, *attrs):
617     """
618     Temporarily delete object attrs and restore afterward.
619     """
620     obj_vals = obj.__dict__ if obj is not None else {}
621     saved_vals = {}
622     for name in attrs:
623         saved_vals[name] = obj_vals.pop(name, _marker)
624     try:
625         yield
626     finally:
627         for name in attrs:
628             saved_val = saved_vals[name]
629             if saved_val is not _marker:
630                 obj_vals[name] = saved_val
631             elif name in obj_vals:
632                 del obj_vals[name]
65dee6 633
DS 634
635 def is_same_domain(host, pattern):
636     """
637     Return ``True`` if the host is either an exact match or a match
638     to the wildcard pattern.
639     Any pattern beginning with a period matches a domain and all of its
640     subdomains. (e.g. ``.example.com`` matches ``example.com`` and
641     ``foo.example.com``). Anything else is an exact string match.
642     """
643     if not pattern:
644         return False
645
646     pattern = pattern.lower()
647     return (pattern[0] == "." and
648             (host.endswith(pattern) or host == pattern[1:]) or
649             pattern == host)
7dd976 650
MM 651
652 def takes_one_arg(callee, attr=None, argname=None):
653     ismethod = False
654     if attr is None:
655         attr = '__call__'
656     if inspect.isroutine(callee):
657         fn = callee
658     elif inspect.isclass(callee):
659         try:
660             fn = callee.__init__
661         except AttributeError:
662             return False
663         ismethod = hasattr(fn, '__call__')
664     else:
665         try:
666             fn = getattr(callee, attr)
667         except AttributeError:
668             return False
669
670     try:
671         argspec = getargspec(fn)
672     except TypeError:
673         return False
674
675     args = argspec[0]
676
677     if hasattr(fn, im_func) or ismethod:
678         # it's an instance method (or unbound method on py2)
679         if not args:
680             return False
681         args = args[1:]
682
683     if not args:
684         return False
685
686     if len(args) == 1:
687         return True
688
689     if argname:
690
691         defaults = argspec[3]
692         if defaults is None:
693             defaults = ()
694
695         if args[0] == argname:
696             if len(args) - len(defaults) == 1:
697                 return True
698
699     return False