mercurial/thirdparty/zope/interface/advice.py
changeset 37176 943d77fc07a3
child 37178 68ee61822182
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/thirdparty/zope/interface/advice.py	Wed Mar 21 19:48:50 2018 -0700
@@ -0,0 +1,205 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Class advice.
+
+This module was adapted from 'protocols.advice', part of the Python
+Enterprise Application Kit (PEAK).  Please notify the PEAK authors
+(pje@telecommunity.com and tsarna@sarna.org) if bugs are found or
+Zope-specific changes are required, so that the PEAK version of this module
+can be kept in sync.
+
+PEAK is a Python application framework that interoperates with (but does
+not require) Zope 3 and Twisted.  It provides tools for manipulating UML
+models, object-relational persistence, aspect-oriented programming, and more.
+Visit the PEAK home page at http://peak.telecommunity.com for more information.
+"""
+
+from types import FunctionType
+try:
+    from types import ClassType
+except ImportError:
+    __python3 = True
+else:
+    __python3 = False
+
+import sys
+
+def getFrameInfo(frame):
+    """Return (kind,module,locals,globals) for a frame
+
+    'kind' is one of "exec", "module", "class", "function call", or "unknown".
+    """
+
+    f_locals = frame.f_locals
+    f_globals = frame.f_globals
+
+    sameNamespace = f_locals is f_globals
+    hasModule = '__module__' in f_locals
+    hasName = '__name__' in f_globals
+
+    sameName = hasModule and hasName
+    sameName = sameName and f_globals['__name__']==f_locals['__module__']
+
+    module = hasName and sys.modules.get(f_globals['__name__']) or None
+
+    namespaceIsModule = module and module.__dict__ is f_globals
+
+    if not namespaceIsModule:
+        # some kind of funky exec
+        kind = "exec"
+    elif sameNamespace and not hasModule:
+        kind = "module"
+    elif sameName and not sameNamespace:
+        kind = "class"
+    elif not sameNamespace:
+        kind = "function call"
+    else: # pragma: no cover
+        # How can you have f_locals is f_globals, and have '__module__' set?
+        # This is probably module-level code, but with a '__module__' variable.
+        kind = "unknown"
+    return kind, module, f_locals, f_globals
+
+
+def addClassAdvisor(callback, depth=2):
+    """Set up 'callback' to be passed the containing class upon creation
+
+    This function is designed to be called by an "advising" function executed
+    in a class suite.  The "advising" function supplies a callback that it
+    wishes to have executed when the containing class is created.  The
+    callback will be given one argument: the newly created containing class.
+    The return value of the callback will be used in place of the class, so
+    the callback should return the input if it does not wish to replace the
+    class.
+
+    The optional 'depth' argument to this function determines the number of
+    frames between this function and the targeted class suite.  'depth'
+    defaults to 2, since this skips this function's frame and one calling
+    function frame.  If you use this function from a function called directly
+    in the class suite, the default will be correct, otherwise you will need
+    to determine the correct depth yourself.
+
+    This function works by installing a special class factory function in
+    place of the '__metaclass__' of the containing class.  Therefore, only
+    callbacks *after* the last '__metaclass__' assignment in the containing
+    class will be executed.  Be sure that classes using "advising" functions
+    declare any '__metaclass__' *first*, to ensure all callbacks are run."""
+    # This entire approach is invalid under Py3K.  Don't even try to fix
+    # the coverage for this block there. :(
+    if __python3: # pragma: no cover
+        raise TypeError('Class advice impossible in Python3')
+
+    frame = sys._getframe(depth)
+    kind, module, caller_locals, caller_globals = getFrameInfo(frame)
+
+    # This causes a problem when zope interfaces are used from doctest.
+    # In these cases, kind == "exec".
+    #
+    #if kind != "class":
+    #    raise SyntaxError(
+    #        "Advice must be in the body of a class statement"
+    #    )
+
+    previousMetaclass = caller_locals.get('__metaclass__')
+    if __python3:   # pragma: no cover
+        defaultMetaclass  = caller_globals.get('__metaclass__', type)
+    else:
+        defaultMetaclass  = caller_globals.get('__metaclass__', ClassType)
+
+
+    def advise(name, bases, cdict):
+
+        if '__metaclass__' in cdict:
+            del cdict['__metaclass__']
+
+        if previousMetaclass is None:
+            if bases:
+                # find best metaclass or use global __metaclass__ if no bases
+                meta = determineMetaclass(bases)
+            else:
+                meta = defaultMetaclass
+
+        elif isClassAdvisor(previousMetaclass):
+            # special case: we can't compute the "true" metaclass here,
+            # so we need to invoke the previous metaclass and let it
+            # figure it out for us (and apply its own advice in the process)
+            meta = previousMetaclass
+
+        else:
+            meta = determineMetaclass(bases, previousMetaclass)
+
+        newClass = meta(name,bases,cdict)
+
+        # this lets the callback replace the class completely, if it wants to
+        return callback(newClass)
+
+    # introspection data only, not used by inner function
+    advise.previousMetaclass = previousMetaclass
+    advise.callback = callback
+
+    # install the advisor
+    caller_locals['__metaclass__'] = advise
+
+
+def isClassAdvisor(ob):
+    """True if 'ob' is a class advisor function"""
+    return isinstance(ob,FunctionType) and hasattr(ob,'previousMetaclass')
+
+
+def determineMetaclass(bases, explicit_mc=None):
+    """Determine metaclass from 1+ bases and optional explicit __metaclass__"""
+
+    meta = [getattr(b,'__class__',type(b)) for b in bases]
+
+    if explicit_mc is not None:
+        # The explicit metaclass needs to be verified for compatibility
+        # as well, and allowed to resolve the incompatible bases, if any
+        meta.append(explicit_mc)
+
+    if len(meta)==1:
+        # easy case
+        return meta[0]
+
+    candidates = minimalBases(meta) # minimal set of metaclasses
+
+    if not candidates: # pragma: no cover
+        # they're all "classic" classes
+        assert(not __python3) # This should not happen under Python 3
+        return ClassType
+
+    elif len(candidates)>1:
+        # We could auto-combine, but for now we won't...
+        raise TypeError("Incompatible metatypes",bases)
+
+    # Just one, return it
+    return candidates[0]
+
+
+def minimalBases(classes):
+    """Reduce a list of base classes to its ordered minimum equivalent"""
+
+    if not __python3: # pragma: no cover
+        classes = [c for c in classes if c is not ClassType]
+    candidates = []
+
+    for m in classes:
+        for n in classes:
+            if issubclass(n,m) and m is not n:
+                break
+        else:
+            # m has no subclasses in 'classes'
+            if m in candidates:
+                candidates.remove(m)    # ensure that we're later in the list
+            candidates.append(m)
+
+    return candidates