#!/usr/bin/env python
#
# Copyright (c) 2011 Jan Kaliszewski (zuo). All rights reserved.
# Licensed under the MIT License.
#
# Python 2.5+/3.x-compatibile.
#
# The newest version of this module should be downloadable from:
# https://github.com/zuo/Zuo-s-Recipes-and-Drafts/blob/master/auxmethods.py
from __future__ import with_statement # (Py2.5 needs this)
from functools import wraps
from inspect import getmro, isfunction
__all__ = (
'ClassNameConflictError',
'aux', 'primary',
'AutoAuxBase', 'AutoAuxMeta',
)
#
# exceptions
class ClassNameConflictError(Exception):
"""
Conflict: class names are identical after stripping leading underscores.
"""
def __str__(self):
cls1, cls2 = self.args
return (
'Class names: %r and %r -- are identical after stripping leading '
'underscores, which is forbidden when using aux/primary methods.'
% (cls1.__name__, cls2.__name__))
#
# non-public stuff
_SUFFIXES = '_primary', '_before', '_after', '_around'
class _WrappedMethodPlaceholder(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
raise TypeError('method placeholder is not callable '
'(forgot to apply aux() class decorator?)')
def _next_around(obj_around, self, basename, *args, **kwargs):
# try to get and call next `around` aux method
meth_around = getattr(obj_around, basename + '_around', None)
if meth_around is not None:
return meth_around(*args, **kwargs)
else:
# if there is no more `around` methods, get and call:
# `before` aux method (it can call superclasses' `before` methods)
meth_before = getattr(self, basename + '_before', None)
if meth_before is not None:
meth_before(*args, **kwargs)
# primary method (it can call superclasses' primary methods)
meth_primary = getattr(self, basename + '_primary')
pri_result = meth_primary(*args, **kwargs)
# `after` aux method (it can call superclasses' `after` methods)
meth_after = getattr(self, basename + '_after', None)
if meth_after is not None:
meth_after(*args, **kwargs)
return pri_result
def _provide_wrapper(cls, func, basename):
@wraps(func)
def wrapper(self, *args, **kwargs):
return _next_around(self, self, basename, *args, **kwargs)
added_doc = '(See: %s%s() signature).' % (basename, '_primary')
existing_doc = (getattr(wrapper, '__doc__', None) or '').rstrip()
if existing_doc:
wrapper.__doc__ = '%s\n\n%s' % (existing_doc, added_doc)
else:
wrapper.__doc__ = added_doc
setattr(cls, basename, wrapper)
def _provide_primary(cls, func, basename):
suffixed_name = basename + '_primary'
func.__name__ = suffixed_name
func.__doc__ = (
'The actual method implementation '
'(%s() is only a wrapper).' % basename)
setattr(cls, suffixed_name, func)
def _provide_wrapped_primary(cls, func):
basename = func.__name__
_provide_wrapper(cls, func, basename)
_provide_primary(cls, func, basename)
def _strip_and_check_cls_name(cls):
cls_stripped_name = cls.__name__.lstrip('_')
for supercls in getmro(cls):
if (supercls is not cls and
cls_stripped_name == supercls.__name__.lstrip('_')):
raise ClassNameConflictError(supercls, cls)
return cls_stripped_name
def _provide_call_next(cls, suffixed_name):
cls_stripped_name = _strip_and_check_cls_name(cls)
basename, qualifier = suffixed_name.rsplit('_', 1)
cn_name = '_%s__%s' % (
cls_stripped_name,
(basename if qualifier == 'primary' else suffixed_name))
if cn_name in vars(cls):
return
if qualifier == 'around':
def call_next(self, *args, **kwargs):
return _next_around(
super(cls, self), self, basename, *args, **kwargs)
else:
def call_next(self, *args, **kwargs):
super_meth = getattr(super(cls, self), suffixed_name, None)
if super_meth is not None:
return super_meth(*args, **kwargs)
call_next.__name__ = cn_name
setattr(cls, cn_name, call_next)
#
# actual decorators
def aux(cls):
"""Class decorator (for classes containing primary and/or aux methods)."""
if not isinstance(cls, type):
raise TypeError('%r is not a type' % cls)
# wrap/rename primary methods
for name, obj in tuple(vars(cls).items()): # (Py2.x/3.x-compatibile way)
if isinstance(obj, _WrappedMethodPlaceholder):
_provide_wrapped_primary(cls, obj.func)
# provide `call-next-method`-like methods
for name, obj in tuple(vars(cls).items()):
if isfunction(obj) and obj.__name__.endswith(_SUFFIXES):
_provide_call_next(cls, obj.__name__)
return cls
def primary(func):
"""Method decorator (for primary methods only)."""
if not isfunction(func):
raise TypeError('%r is not a function' % func)
return _WrappedMethodPlaceholder(func)
#
# convenience classes (any of them can be used *optionally*...)
class AutoAuxMeta(type):
"""Convenience metaclass: `aux()`-decorates classes created by it."""
def __new__(mcs, name, bases, attr_dict):
return aux(type.__new__(mcs, name, bases, attr_dict))
# (here: Py2.x/3.x-compatibile way to create a class with a custom metaclass)
AutoAuxBase = AutoAuxMeta('AutoAuxBase', (object,), {'__doc__':
"""`AutoAuxMeta`-created base class: `aux()`-decorates its subclasses."""})
#
# basic example
if __name__ == '__main__':
import sys
import time
class TimedAction(AutoAuxBase):
# note: AutoAuxBase automatically decorates your classes with aux()
def action_before(self, *args, **kwargs):
"""Start action timer."""
print('starting action timer...')
self.start_time = time.time()
def action_after(self, *args, **kwargs):
"""Stop action timer and report measured duration."""
self.action_duration = time.time() - self.start_time
print('action duration: %f' % self.action_duration)
class FileContentAction(AutoAuxBase):
def action_around(self, path):
"""Read file and pass its content on; report success or error."""
print('opening file %r...' % path)
try:
with open(path) as f:
content = f.read()
except EnvironmentError:
print(sys.exc_info()[1])
else:
result = self.__action_around(path, content)
print('file %r processed successfully' % path)
return result
class NewlinesCounter(FileContentAction, TimedAction):
item_descr = 'newlines'
@primary
def action(self, path, content):
"""Get number of newlines in a given string."""
return content.count('\n')
def action_before(self, path, *args):
"""Print a message and go on..."""
print('counting %s in file %r will start...' % (
self.item_descr, path))
self.__action_before(path, *args)
def action_around(self, path):
"""Start operation with given file path. Finally, show summary."""
result = self.__action_around(path)
if result is not None:
print('%s in file %r: %s\n' % (
self.item_descr, path, result))
else:
print('could not count %s in file %r\n' % (
self.item_descr, path))
return result
class SpacesAndNewlinesCounter(NewlinesCounter):
item_descr = 'spaces and newlines'
@primary
def action(self, path, content):
"""Get number of spaces and newlines in a given string."""
spaces = content.count(' ')
newlines = self.__action(path, content)
return spaces + newlines
example_file_paths = __file__, 'spam/spam/spam/non-existent'
nl_counter = NewlinesCounter()
spc_nl_counter = SpacesAndNewlinesCounter()
for path in example_file_paths:
nl_counter.action(path)
spc_nl_counter.action(path)