[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
|