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