class Property(_Comparable):
    """
    I am a property on a class that maps to database column.

    @ivar attr_name: Name of the attribute on the class
    @ivar column_name: Name of the column in the database.
    @ivar primary: C{True} if I am a part of the primary key.
    @ivar cls: The Class I live on.
    """

    _value_dict = _WeakIdentityDict()
    _changes = _WeakIdentityDict()
    attr_name = None
    cls = None
    primary = False


    def __init__(self, column_name=None, primary=False, fromDatabase=None,
                 toDatabase=None, default_factory=None, validators=None):
        """
        @param column_name: Name of the column in the database if different
            than the name being used in the class.  Meaning, these two lines
            have an equivalent C{column_name}:

                foo = Property()
                foo = Property('foo')

        @param primary: C{True} means this column is part of the primary key
        @param fromDatabase: A function to be called to convert a value from
            the database into one ready for python use.  This should be the
            inverse of C{toDatabase}.
        @param toDatabase: A function that will be called to convert a value
            from python-land to database-land.  This should be the inverse
            of C{fromDatabase}.
        @param default_factory: A function accepting no arguments that will
            be called to compute a default value for this attribute.
        @param validators: A list of functions to validate values being
            assigned to this attribute.  Each function will be called with
            three arguments:
                - prop: (me, the thing you're instantiating with this __init__)
                - obj: The object on which the value is being set
                - value: The value being set.
            Each validator will be called in the order given.
        """
        self.column_name = column_name
        self._fromDatabase = fromDatabase or (lambda x:x)
        self._toDatabase = toDatabase or (lambda x:x)
        self._default_factory = default_factory
        self.validators = validators or []
        self.validators.append(self._validate)
        self.primary = primary


    def __get__(self, obj, cls):
        if not self.attr_name:
            self._cacheAttrName(cls)
        if obj:
            return self._getValue(obj)
        return self


    def __set__(self, obj, val):
        self._setValue(obj, val)


    def _setValue(self, obj, value, record_change=True):
        if not self.attr_name:
            self._cacheAttrName(obj.__class__)
        new_value = value
        for v in self.validators:
            new_value = v(self, obj, new_value)
        self._values(obj)[self.attr_name] = new_value
        if record_change:
            self._markChanged(obj)


    def _getValue(self, obj):
        if not self.attr_name:
            self._cacheAttrName(obj.__class__)
        values = self._values(obj)
        try:
            return values[self.attr_name]
        except KeyError:
            if self._default_factory:
                self._setValue(obj, self._default_factory())
            else:
                self._setValue(obj, None, record_change=False)
            return values[self.attr_name]


    def _values(self, obj):
        return self._value_dict.setdefault(obj, {})


    def _validate(self, prop, obj, value):
        """
        Override this in subclasses for default validation.
        """
        return value


    def valueFor(self, obj):
        """
        Return the python-land value of this property for the given object.
        """
        return self._getValue(obj)


    def _markChanged(self, obj):
        self.changes(obj).add(self)


    def changes(self, obj):
        """
        Get a list of changes on this object (not just for this property).
        """
        for prop in classInfo(obj).attributes.values():
            # this is so that default values are populated
            prop.valueFor(obj)
        if self._changes.get(obj, None) is None:
            self._changes[obj] = set()
        return self._changes[obj]


    def _cacheAttrName(self, cls):
        for attr in dir(cls):
            try:
                prop = cls.__dict__[attr]
            except:
                continue
            if isinstance(prop, Property):
                prop.attr_name = attr
                prop.column_name = prop.column_name or attr
                prop.cls = cls


    def toDatabase(self, obj):
        """
        Get the value of this L{Property} for the given object converted for
        a database.

        @param obj: The obj on which this descriptor lives.

        @return: A database-ready value
        """
        return self._toDatabase(self._getValue(obj))


    def fromDatabase(self, obj, value):
        """
        Set the value of this attribute as from a database, doing any applicable
        conversions for this attribute.

        Setting the value of an attribute using me will reset the changed status
        of the attribute.

        @param obj: The obj on which this descriptor lives.
        @param value: The value returned by the database.
        """
        self._setValue(obj, self._fromDatabase(value), record_change=False)
        try:
            self.changes(obj).remove(self)
        except:
            pass


    def __repr__(self):
        name = self.attr_name
        if self.attr_name != self.column_name:
            name = '%s (aka %s)' % (name, self.column_name)
        return '<Property %s of %r 0x%x>' % (name, self.cls, id(self)) def classInfo(cls):
    # XXX implement caching if it's a big deal
    if not inspect.isclass(cls):
        cls = cls.__class__
    return _ClassInfo(cls) def objectInfo(obj):
    """
    Get ORM-related information about an object.  See L{_ObjectInfo} for more
    details.
    """
    # XXX implement caching if it's a big deal
    return _ObjectInfo(obj) def reconstitute(data):
    """
    Reconstitute an object or list of objects using data from a database.
    This will not call the C{__init__} method of the reconstituted classes.

    @param data: An iterable of 2-tuples (property, value) to be used to set
        the values on the returned instance(s).

    @return: XXX 'spain this, Lucy
    """
    classes = []
    class_data = defaultdict(lambda:[])
    for prop, value in data:
        if prop.cls not in classes:
            classes.append(prop.cls)
        class_data[prop.cls].append((prop, value))
    
    ret = []
    for cls in classes:
        obj = cls.__new__(cls)
        empty_obj = True
        for prop, value in class_data[cls]:
            if empty_obj and value is not None:
                empty_obj = False
            prop.fromDatabase(obj, value)
        if empty_obj:
            # No values, None (from a Left Join or the like)
            ret.append(None)
        else:
            ret.append(obj)

    if len(ret) == 1:
        return ret[0]
    return ret class Converter(object):
    """
    I let you register conversion functions for types, then use the conversion
    functions later by passing in the type.  See my tests for usage.
    """

    def __init__(self):
        self.converters = defaultdict(lambda: [])


    def when(self, key):
        """
        """
        def deco(f):
            self.converters[key].append(f)
            return f
        return deco


    def convert(self, key, value):
        """
        XXX
        """
        for conv in self.converters[key]:
            value = conv(value)
        return value class Eq(Comparison):
    op = '=' class Neq(Comparison):
    op = '!=' class Gt(Comparison):
    op = '>' class Gte(Comparison):
    op = '>=' class Lt(Comparison):
    op = '<' class Lte(Comparison):
    op = '<='
from twisted.trial.unittest import TestCase
from norm.orm.base import (Property, classInfo, objectInfo, reconstitute,
                           Converter)
from norm.orm.expr import Eq, Neq, Gt, Gte, Lt, Lte
        info.resetChangedList()
        self.assertEqual(info.changed(), [], "Nothing has changed since "
                         "the change list was cleared")
        
        foo.b = 'hey'
        self.assertEqual(info.changed(), [Foo.b])


    def test_changed_default(self):
        """
        Specifying a default will be included in the changes
        """
        class Foo(object):
            a = Property(default_factory=lambda:10)

        foo = Foo()
        info = objectInfo(foo)
        changed = info.changed()
        self.assertEqual(changed, [Foo.a])



class reconstituteTest(TestCase):


    def test_tuple(self):
        """
        You can reconstitute a class with a tuple of attributes.
        """
        class Foo(object):
            a = Property()
            b = Property()


        zipped = zip([Foo.a, Foo.b], [1, 'hello'])
        foo = reconstitute(zipped)
        self.assertTrue(isinstance(foo, Foo))
        self.assertEqual(foo.a, 1)
        self.assertEqual(foo.b, 'hello')


    def test_init(self):
        """
        __init__ should not be called
        """
        class Foo(object):
            a = Property()
            b = Property()
            init_called = False

            def __init__(self):
                self.init_called = True


        foo = reconstitute([(Foo.a, 'hey'), (Foo.b, 'something')])
        self.assertTrue(isinstance(foo, Foo))
        self.assertEqual(foo.init_called, False, "Should not have called "
                         "__init__")


    def test_fromDatabase(self):
        """
        The values should be mutated according to the rules of fromDatabase
        """
        class Foo(object):
            a = Property(fromDatabase=(lambda x:x+'db'))


        foo = reconstitute([(Foo.a, 'hey')])
        self.assertEqual(foo.a, 'heydb')


    def test_multiple(self):
        """
        If multiple classes are represented in the properties, return multiple classes
        """
        class Foo(object):
            a = Property()
        class Bar(object):
            b = Property()

        foo, bar = reconstitute([
            (Foo.a, 'hey'),
            (Bar.b, 'ho'),
        ])
        self.assertEqual(foo.a, 'hey')
        self.assertEqual(bar.b, 'ho')



class ConverterTest(TestCase):


    def test_identity(self):
        """
        By default, values are left as is
        """
        conv = Converter()
        val = conv.convert(Property, 'foo')
        self.assertEqual(val, 'foo')


    def test_function(self):
        """
        You can use a function for converting.
        """
        conv = Converter()

        @conv.when(Property)
        def converter(x):
            return x + 'hey'

        val = conv.convert(Property, 'foo')
        self.assertEqual(val, 'foohey')


    def test_multiple(self):
        """
        You can have multiple functions do conversion
        """

        conv = Converter()