214 lines
7.4 KiB
Python
214 lines
7.4 KiB
Python
##############################################################################
|
|
#
|
|
# 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
|
|
|
|
__all__ = [
|
|
'addClassAdvisor',
|
|
'determineMetaclass',
|
|
'getFrameInfo',
|
|
'isClassAdvisor',
|
|
'minimalBases',
|
|
]
|
|
|
|
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
|