advice.py
207 lines
| 7.3 KiB
| text/x-python
|
PythonLexer
Gregory Szorc
|
r37193 | ############################################################################## | ||
# | ||||
# 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. | ||||
""" | ||||
Gregory Szorc
|
r37195 | from __future__ import absolute_import | ||
Gregory Szorc
|
r37193 | 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 | ||||