#!/usr/bin/python -tt
# vim:ts=8:sw=4:et:tw=72:
# selfish.py - Metaclasses to simplify object initialization

# Copyright (C) 2003 Thomas Schumm <phong@phong.org> =============== {{{
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
# OF THIRD PARTY RIGHTS.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
# SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.           }}}

# Module documentation ============================================= {{{

"""Provides metaclasses to simplifiy object initialization.

Normally, to initialize an object from its parameters, the names all
must be listed in triplicate.  For example:

    def __init__(self, foo, bar, baz):
        self.foo, self.bar, self.baz = foo, bar, baz
        # do other stuff

The metaclasses in this module eliminate this duplication.  To use them,
simply inherit your classes from Selfish or SelfishLight, or set the
__metaclass__ in your class.  Your __init__ can then be written like
this:

    def __init__(self, _foo, _bar, _baz):
        # do other stuff

When your class is defined, the metaclass examines the argument list for
your __init__ and creates a new method that handles all the
initialization and calls your __init__ (which is renamed to _meta_init).

Underscore is treated as a 'magic' character.  Any parameters in your
__init__ starting with underscore get assigned to object attributes:

    def __init__(self, _foo, bar):
        print self.foo          # <=  works
        print self.bar          # <=  raises an exception

Naturally, the underscore is stripped.  If callers of your class use
named parameters, they do not include the leading underscore.  In other
words, they are only present in your original __init__'s parameter list.

There is no restriction on the use of *var_args, **keyword_args or
default values; e.g. the following is possible:

    def __init__(self, _size=1, *_args, **_kw):
        print self.size         # <=  1 if not specified by caller
        print type(self.args)   # <=  <type 'list'>
        print type(self.kw)     # <=  <type 'dict'>

There are two versions of the helper metaclass - one that uses the
compiler, and one that doesn't.  Generally, the compiled version should
be preferred.  It is more flexible and, based on cursory benchmarks,
ultimately about 3-4 times faster for object instance creation.

Some features are in a state of flux and may change in future versions.
For example, the meaning of '_' may be inverted in the future (i.e. you
get magic unless you request non-magic.)

""" # }}}

__version__ = "0.6.1"
__author__ = "Thomas Schumm <phong@phong.org>"

# These are the predefined flags for <code object>.co_flags
CO_OPTIMIZED = 0x0001
CO_NEWLOCALS = 0x0002
CO_VARARGS = 0x0004
CO_VARKEYWORDS = 0x0008

def func_args(func):
    """Examine a function or method; return information about its arguments.

    Returns a tuple with four elements:
        params   - a list of all argument names (except var_arg, kw_arg)
        defaults - a dict for those arguments with default values
        var_arg  - the name of the *args argument (or None)
        kw_arg   - the name of the **kw argument (or None)

    """
    try:
        func = func.im_func # Handle instance methods too, for completeness
    except AttributeError:
        pass
    code = func.func_code
    func_defs = func.func_defaults or ()
    num_args = code.co_argcount
    params = code.co_varnames[:num_args]
    defaults = dict(zip(params[-len(func_defs):], func_defs))

    if code.co_flags & CO_VARARGS:
        var_arg = code.co_varnames[num_args]
        num_args += 1
    else:
        var_arg = None
    if code.co_flags & CO_VARKEYWORDS:
        kw_arg = code.co_varnames[num_args]
    else:
        kw_arg = None

    return params, defaults, var_arg, kw_arg

class MetaInitializer(type):
    """Metaclass for SelfishLight classes (does not use the compiler)."""

    def make_init(cls, old_init, params, defaults, var_arg, kw_arg):
        """Creates the new __init__ function."""
        p_params = params[1:] # Don't include self
        # This is starting to get ugly - I should rewrite it.
        def __init__(self, *args, **kw):
            setkw = kw_arg != None and kw_arg[0] == '_'
            if setkw:
                vkw = kw.copy()
            for name, value in zip(p_params, args):
                if name[0] != '_': continue
                setattr(self, name[1:], value)
            for name in p_params[len(args):]:
                if name[0] != '_': continue
                sname = name[1:]
                if sname in kw:
                    setattr(self, sname, kw[sname])
                    if name in kw:
                        raise TypeError("__init__() got multiple values "
                                        "for keyword argument '%s'" % name)
                    kw[name] = kw[sname]
                    del(kw[sname])
                    if setkw:
                        del(vkw[sname])
                elif name in defaults:
                    setattr(self, sname, defaults[name])
            if var_arg != None and var_arg[0] == '_':
                setattr(self, var_arg[1:], args[len(p_params):])
            if setkw:
                setattr(self, kw_arg[1:], vkw)
            # Original raises TypeError if a required parameter is missing.
            self._meta_init(*args, **kw)
        return __init__

    def __init__(cls, name, bases, class_dict):
        """Replaces __init__ on class creation, if present."""
        super(MetaInitializer, cls).__init__(cls, name, bases, class_dict)
        try:
            old_init = class_dict['__init__']
        except KeyError:
            return

        cls._meta_init = old_init
        cls.__init__ = cls.make_init(old_init, *func_args(old_init))
        cls.__init__.im_func.__doc__ = cls._meta_init.im_func.__doc__

class MetaInitializerCompiled(MetaInitializer):
    """Metaclass for Selfish classes (uses the compiler)."""

    def make_init(cls, old_init, params, defaults, var_arg, kw_arg):
        """Creates and returns the new __init__ function."""
        orig_args = list(params[1:]) # Don't include self in assigments
        call_args = [ p[0] != '_' and p or p[1:] for p in params ]
        num = len(defaults)
        if num > 0:
            para_args = call_args[:-num]
            # changed from orig_args to params to produce slightly less
            # broken behavior if some weirdo specifies a default value
            # for the "self" parameter
            para_args.extend([ "%s=defaults['%s']" % (n, o)
                    for n, o in zip(call_args[-num:], params[-num:]) ])
        else:
            para_args = call_args[:]
        for arg, typ in (var_arg, '*'), (kw_arg, '**'):
            if arg != None:
                orig_args.append(arg)
                if arg[0] == '_': arg = arg[1:]
                t = typ + arg
                call_args.append(t)
                para_args.append(t)

        func_code = 'def __init__(%s):\n' % ", ".join(para_args)
        func_code += "\n".join([
                "\t%s.%s = %s" % (call_args[0], p[1:], p[1:])
                for p in orig_args if p[0] == '_' ])
        func_code += "\n\t%s._meta_init(%s)\n" % \
                (call_args[0], ", ".join(call_args[1:]))
        #+ print func_code
        exec func_code in locals()
        return __init__

class SelfishLight(object):
    """A class that initializes its instance attributes automatically.

    See the module documentation for usage.

    This version uses the regular MetaInitializer metaclass.  Unlike the
    compiled version, the parameters for the new __init__ do not match
    the original.

    It also generates a "general purpose" intializer which is a fair
    amount slower than the one produced by MetaInitializerCompiled, but
    using the compiler feels like cheating so I've kept this "pure" one
    here.  The compiled version should be prefered unless you can't or
    don't want to use the compiler.

    """
    __metaclass__ = MetaInitializer

class Selfish(object):
    """A class that initializes its instance attributes automatically.

    See the module documentation for usage.

    This version uses the MetaInitializerCompiled metaclass which uses
    eval to generate the new __init__.  Instance creation is
    significantly faster than SelfishLight.  The parameter list for the
    created __init__ also closely resembles the original.

    """
    __metaclass__ = MetaInitializerCompiled

if __name__ == "__main__":
    print __doc__
