[SQL-CVS] r572 - in trunk/SQLObject/sqlobject: . tests
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: <sub...@co...> - 2005-02-08 07:45:29
|
Author: ianb Date: 2005-02-08 07:45:24 +0000 (Tue, 08 Feb 2005) New Revision: 572 Added: trunk/SQLObject/sqlobject/boundattributes.py trunk/SQLObject/sqlobject/declarative.py trunk/SQLObject/sqlobject/tests/test_boundattributes.py trunk/SQLObject/sqlobject/tests/test_declarative.py Log: Added declarative and boundattributes, which will be the basis for the new SQLObject metaclass Added: trunk/SQLObject/sqlobject/boundattributes.py =================================================================== --- trunk/SQLObject/sqlobject/boundattributes.py 2005-02-07 11:02:13 UTC (rev 571) +++ trunk/SQLObject/sqlobject/boundattributes.py 2005-02-08 07:45:24 UTC (rev 572) @@ -0,0 +1,124 @@ +""" +Bound attributes are attributes that are bound to a specific class and +a specific name. In SQLObject a typical example is a column object, +which knows its name and class. + +A bound attribute should define a method ``__addtoclass__(added_class, +name)`` (attributes without this method will simply be treated as +normal). The return value is ignored; if the attribute wishes to +change the value in the class, it must call ``setattr(added_class, +name, new_value)``. + +BoundAttribute is a class that facilitates lazy attribute creation. + +``bind_attributes(cls, new_attrs)`` is a function that looks for +attributes with this special method. ``new_attrs`` is a dictionary, +as typically passed into ``__classinit__`` with declarative (calling +``bind_attributes`` in ``__classinit__`` would be typical). + +Note if you do this that attributes defined in a superclass will not +be rebound in subclasses. If you want to rebind attributes in +subclasses, use ``bind_attributes_local``, which adds a +``__bound_attributes__`` variable to your class to track these active +attributes. +""" + +__all__ = ['BoundAttribute', 'BoundFactory', 'bind_attributes', + 'bind_attributes_local'] + +import declarative + +class BoundAttribute(declarative.Declarative): + + """ + This is a declarative class that passes all the values given to it + to another object. So you can pass it arguments (via + __init__/__call__) or give it the equivalent of keyword arguments + through subclassing. Then a bound object will be added in its + place. + + To hook this other object in, override ``make_object(added_class, + name, **attrs)`` and maybe ``set_object(added_class, name, + **attrs)`` (the default implementation of ``set_object`` + just resets the attribute to whatever ``make_object`` returned). + """ + + _private_variables = ( + '_private_variables', + '_all_attributes', + '__classinit__', + '__addtoclass__', + '_add_attrs', + 'set_object', + 'make_object', + ) + + _all_attrs = () + + def __classinit__(cls, new_attrs): + declarative.Declarative.__classinit__(cls, new_attrs) + cls._all_attrs = cls._add_attrs(cls, new_attrs) + + def __instanceinit__(self, new_attrs): + declarative.Declarative.__instanceinit__(self, new_attrs) + self._all_attrs = self._add_attrs(self, new_attrs) + + def _add_attrs(this_object, new_attrs): + private = this_object._private_variables + all_attrs = list(this_object._all_attrs) + for key in new_attrs.keys(): + if key.startswith('_') or key in private: + continue + if key not in all_attrs: + all_attrs.append(key) + return tuple(all_attrs) + _add_attrs = staticmethod(_add_attrs) + + def __addtoclass__(self, cls, added_class, attr_name): + me = self or cls + attrs = {} + for name in me._all_attrs: + attrs[name] = getattr(me, name) + attrs['added_class'] = added_class + attrs['attr_name'] = attr_name + obj = me.make_object(**attrs) + me.set_object(added_class, attr_name, obj) + + __addtoclass__ = declarative.classinstancemethod(__addtoclass__) + + def set_object(cls, added_class, attr_name, obj): + setattr(added_class, attr_name, obj) + + set_object = classmethod(set_object) + + def make_object(cls, added_class, attr_name, *args, **attrs): + raise NotImplementedError + + make_object = classmethod(make_object) + +class BoundFactory(BoundAttribute): + + factory_class = None + + def make_object(cls, *args, **kw): + return cls.factory_class(*args, **kw) + +def bind_attributes(cls, new_attrs): + for name, value in new_attrs.items(): + if hasattr(value, '__addtoclass__'): + value.__addtoclass__(cls, name) + +def bind_attributes_local(cls, new_attrs): + new_bound_attributes = {} + for name, value in getattr(cls, '__bound_attributes__', {}).items(): + if new_attrs.has_key(name): + # The attribute is being REbound, so don't try to bind it + # again. + continue + value.__addtoclass__(cls, name) + new_bound_attributes[name] = value + for name, value in new_attrs.items(): + if hasattr(value, '__addtoclass__'): + value.__addtoclass__(cls, name) + new_bound_attributes[name] = value + cls.__bound_attributes__ = new_bound_attributes Copied: trunk/SQLObject/sqlobject/declarative.py (from rev 552, trunk/Validator/validator/declarative.py) =================================================================== --- trunk/Validator/validator/declarative.py 2005-01-24 12:10:39 UTC (rev 552) +++ trunk/SQLObject/sqlobject/declarative.py 2005-02-08 07:45:24 UTC (rev 572) @@ -0,0 +1,192 @@ +""" +Declarative objects. + +Declarative objects have a simple protocol: you can use classes in +lieu of instances and they are equivalent, and any keyword arguments +you give to the constructor will override those instance variables. +(So if a class is received, we'll simply instantiate an instance with +no arguments). + +You can provide a variable __unpackargs__ (a list of strings), and if +the constructor is called with non-keyword arguments they will be +interpreted as the given keyword arguments. + +If __unpackargs__ is ('*', name), then all the arguments will be put +in a variable by that name. + +You can define a __classinit__(cls, new_attrs) method, which will be +called when the class is created (including subclasses). Note: you +can't use super() in __classinit__ because the class isn't bound to a +name. As an analog to __classinit__, Declarative adds +__instanceinit__ which is called with the same argument (new_attrs). +This is like __init__, but after __unpackargs__ and other factors have +been taken into account. + +If __mutableattributes__ is defined as a sequence of strings, these +attributes will not be shared between superclasses and their +subclasses. E.g., if you have a class variable that contains a list +and you append to that list, changes to subclasses will effect +superclasses unless you add the attribute here. + +Also defines classinstancemethod, which acts as either a class method +or an instance method depending on where it is called. +""" + +from __future__ import generators + +__all__ = ('classinstancemethod', 'DeclarativeMeta', 'Declarative') + +import copy + +try: + import itertools + counter = itertools.count() +except ImportError: + def _counter(): + i = 0 + while 1: + i += 1 + yield i + counter = _counter() + +class classinstancemethod(object): + """ + Acts like a class method when called from a class, like an + instance method when called by an instance. The method should + take two arguments, 'self' and 'cls'; one of these will be None + depending on how the method was called. + """ + + def __init__(self, func): + self.func = func + + def __get__(self, obj, type=None): + return _methodwrapper(self.func, obj=obj, type=type) + +class _methodwrapper(object): + + def __init__(self, func, obj, type): + self.func = func + self.obj = obj + self.type = type + + def __call__(self, *args, **kw): + assert not kw.has_key('self') and not kw.has_key('cls'), ( + "You cannot use 'self' or 'cls' arguments to a " + "classinstancemethod") + return self.func(*((self.obj, self.type) + args), **kw) + + def __repr__(self): + if self.obj is None: + return ('<bound class method %s.%s>' + % (self.type.__name__, self.func.func_name)) + else: + return ('<bound method %s.%s of %r>' + % (self.type.__name__, self.func.func_name, self.obj)) + + +class DeclarativeMeta(type): + + def __new__(meta, class_name, bases, new_attrs): + cls = type.__new__(meta, class_name, bases, new_attrs) + if new_attrs.has_key('__classinit__'): + cls.__classinit__ = staticmethod(cls.__classinit__.im_func) + cls.declarative_count = counter.next() + cls.__classinit__(cls, new_attrs) + return cls + +class Declarative(object): + + __unpackargs__ = () + + __mutableattributes__ = () + + __metaclass__ = DeclarativeMeta + + def __classinit__(cls, new_attrs): + for name in cls.__mutableattributes__: + if not new_attrs.has_key(name): + setattr(cls, copy.copy(getattr(cls, name))) + + def __instanceinit__(self, new_attrs): + for name, value in new_attrs.items(): + setattr(self, name, value) + if not new_attrs.has_key('declarative_count'): + self.declarative_count = counter.next() + + def __init__(self, *args, **kw): + if self.__unpackargs__ and self.__unpackargs__[0] == '*': + assert len(self.__unpackargs__) == 2, \ + "When using __unpackargs__ = ('*', varname), you must only provide a single variable name (you gave %r)" % self.__unpackargs__ + name = self.__unpackargs__[1] + if kw.has_key(name): + raise TypeError( + "keyword parameter '%s' was given by position and name" + % name) + kw[name] = args + else: + if len(args) > len(self.__unpackargs__): + raise TypeError( + '%s() takes at most %i arguments (%i given)' + % (self.__class__.__name__, + len(self.__unpackargs__), + len(args))) + for name, arg in zip(self.__unpackargs__, args): + if kw.has_key(name): + raise TypeError( + "keyword parameter '%s' was given by position and name" + % name) + kw[name] = arg + if kw.has_key('__alsocopy'): + for name, value in kw['__alsocopy'].items(): + if not kw.has_key(name): + if name in self.__mutableattributes__: + value = copy.copy(value) + kw[name] = value + del kw['__alsocopy'] + self.__instanceinit__(kw) + + def __call__(self, *args, **kw): + kw['__alsocopy'] = self.__dict__ + return self.__class__(*args, **kw) + + def singleton(self, cls): + if self: + return self + name = '_%s__singleton' % cls.__name__ + if not hasattr(cls, name): + setattr(cls, name, cls(declarative_count=cls.declarative_count)) + return getattr(cls, name) + singleton = classinstancemethod(singleton) + + def __repr__(self, cls): + if self: + name = '%s object' % self.__class__.__name__ + v = self.__dict__.copy() + else: + name = '%s class' % cls.__name__ + v = cls.__dict__.copy() + if v.has_key('declarative_count'): + name = '%s %i' % (name, v['declarative_count']) + del v['declarative_count'] + # @@: simplifying repr: + #v = {} + names = v.keys() + args = [] + for n in self._repr_vars(names): + args.append('%s=%r' % (n, v[n])) + if not args: + return '<%s>' % name + else: + return '<%s %s>' % (name, ' '.join(args)) + + def _repr_vars(dictNames): + names = [n for n in dictNames + if not n.startswith('_') + and n != 'declarative_count'] + names.sort() + return names + _repr_vars = staticmethod(_repr_vars) + + __repr__ = classinstancemethod(__repr__) + Added: trunk/SQLObject/sqlobject/tests/test_boundattributes.py =================================================================== --- trunk/SQLObject/sqlobject/tests/test_boundattributes.py 2005-02-07 11:02:13 UTC (rev 571) +++ trunk/SQLObject/sqlobject/tests/test_boundattributes.py 2005-02-08 07:45:24 UTC (rev 572) @@ -0,0 +1,57 @@ +from sqlobject import declarative +from sqlobject import boundattributes + +class TestMe(object): + + __metaclass__ = declarative.DeclarativeMeta + __classinit__ = boundattributes.bind_attributes_local + +class AttrReplace(boundattributes.BoundAttribute): + + __unpackargs__ = ('replace',) + + replace = None + + def make_object(self, cls, added_class, attr_name, **attrs): + if not self: + return cls.singleton().make_object( + added_class, attr_name, **attrs) + self.replace.added_class = added_class + self.replace.name = attr_name + assert attrs['replace'] is self.replace + del attrs['replace'] + self.replace.attrs = attrs + return self.replace + + make_object = declarative.classinstancemethod(make_object) + +class Holder: + def __init__(self, name): + self.holder_name = name + def __repr__(self): + return '<Holder %s>' % self.holder_name + +def test_1(): + v1 = Holder('v1') + v2 = Holder('v2') + v3 = Holder('v3') + class V2Class(AttrReplace): + arg1 = 'nothing' + arg2 = ['something'] + class A1(TestMe): + a = AttrReplace(v1) + v = V2Class(v2) + class inline(AttrReplace): + replace = v3 + arg3 = 'again' + arg4 = 'so there' + for n in ('a', 'v', 'inline'): + assert getattr(A1, n).name == n + assert getattr(A1, n).added_class is A1 + assert A1.a is v1 + assert A1.a.attrs == {} + assert A1.v is v2 + assert A1.v.attrs == {'arg1': 'nothing', 'arg2': ['something']} + assert A1.inline is v3 + assert A1.inline.attrs == {'arg3': 'again', 'arg4': 'so there'} + Added: trunk/SQLObject/sqlobject/tests/test_declarative.py =================================================================== --- trunk/SQLObject/sqlobject/tests/test_declarative.py 2005-02-07 11:02:13 UTC (rev 571) +++ trunk/SQLObject/sqlobject/tests/test_declarative.py 2005-02-08 07:45:24 UTC (rev 572) @@ -0,0 +1,78 @@ +from sqlobject.declarative import * + +class A1(Declarative): + + a = 1 + b = [] + +class A2(A1): + + a = 5 + +A3 = A2(b=5) + +def test_a_classes(): + assert A1.a == 1 + assert A1.singleton().a == 1 + assert A1.b is A2.b + assert A3.b == 5 + assert A1.declarative_count == A1.singleton().declarative_count + assert A1.declarative_count < A2.declarative_count + assert A2.singleton() is not A1.singleton() + assert A3.singleton().b == A3.b + +class B1(Declarative): + + attrs = [] + + def __classinit__(cls, new_attrs): + Declarative.__classinit__(cls, new_attrs) + cls.attrs = cls.add_attrs(cls.attrs, new_attrs) + + def __instanceinit__(self, new_attrs): + Declarative.__instanceinit__(self, new_attrs) + self.attrs = self.add_attrs(self.attrs, new_attrs) + + def add_attrs(old_attrs, new_attrs): + old_attrs = old_attrs[:] + for name in new_attrs.keys(): + if (name in old_attrs + or name.startswith('_') + or name in ('add_attrs', 'declarative_count', 'attrs')): + continue + old_attrs.append(name) + old_attrs.sort() + return old_attrs + add_attrs = staticmethod(add_attrs) + + c = 1 + +class B2(B1): + + g = 3 + + def __classinit__(cls, new_attrs): + new_attrs['test'] = 'whatever' + B1.__classinit__(cls, new_attrs) + +B3 = B2(c=5, d=3) +B4 = B3(d=5) +B5 = B1(a=1) + +def test_b_classes(): + assert B1.attrs == ['c'] + assert B1.c == 1 + assert B2.attrs == ['c', 'g', 'test'] + assert B3.d == 3 + assert B4.d == 5 + assert B5.a == 1 + assert B5.attrs == ['a', 'c'] + assert B3.attrs == ['c', 'd', 'g', 'test'] + assert B4.attrs == ['c', 'd', 'g', 'test'] + order = [B1, B1.singleton(), B2, B2.singleton(), + B3, B3.singleton(), B4, B4.singleton(), + B5, B5.singleton()] + last = 0 + for obj in order: + assert obj.declarative_count >= last + last = obj.declarative_count |