#!/usr/bin/env python
"""Unit tests for parser support libraries dealing with records.
"""
from cogent.parse.record import FieldError, RecordError, Grouper, \
    DelimitedSplitter, GenericRecord, MappedRecord, \
    TypeSetter, list_adder, dict_adder, \
    LineOrientedConstructor, int_setter, str_setter, bool_setter, \
    string_and_strip, FieldWrapper, StrictFieldWrapper, raise_unknown_field, \
    FieldMorpher, list_extender

from cogent.util.unit_test import TestCase, main

__author__ = "Rob Knight"
__copyright__ = "Copyright 2007-2012, The Cogent Project"
__credits__ = ["Rob Knight"]
__license__ = "GPL"
__version__ = "1.5.3"
__maintainer__ = "Rob Knight"
__email__ = "rob@spot.colorado.edu"
__status__ = "Production"

class recordsTests(TestCase):
    """Tests of top-level functionality in records."""
    def test_string_and_strip(self):
        """string_and_strip should convert all items to strings and strip them"""
        self.assertEqual(string_and_strip(), [])
        self.assertEqual(string_and_strip('\t', ' ', '\n\t'), ['','',''])
        self.assertEqual(string_and_strip('\ta\tb', 3, '   cde   e', None), \
            ['a\tb', '3', 'cde   e', 'None'])

    def test_raise_unknown_field(self):
        """raise_unknown_field should always raise FieldError"""
        self.assertRaises(FieldError, raise_unknown_field, 'xyz', 123)

class GrouperTests(TestCase):
    """Tests of the Grouper class."""
    def test_call(self):
        """Grouper should return lists containing correct number of groups"""
        empty = []
        s3 = 'abc'
        s10 = range(10)
        g1 = Grouper(1)
        g2 = Grouper(2)
        g5 = Grouper(5)
        self.assertEqual(list(g1(empty)), [])
        self.assertEqual(list(g2(empty)), [])
        self.assertEqual(list(g5(empty)), [])
        self.assertEqual(list(g1(s3)), [['a'], ['b'], ['c']])
        self.assertEqual(list(g2(s3)), [['a','b'], ['c']])
        self.assertEqual(list(g5(s3)), [['a','b','c']])
        self.assertEqual(list(g1(s10)), [[i] for i in range(10)])
        self.assertEqual(list(g2(s10)), [[0,1],[2,3],[4,5],[6,7],[8,9]])
        self.assertEqual(list(g5(s10)), [[0,1,2,3,4],[5,6,7,8,9]])

    def test_call_bad(self):
        """Grouper call should raise ValueError if NumItems is not an int"""
        g_none = Grouper(None)
        g_neg = Grouper(-1)
        g_zero = Grouper(0)
        g_alpha = Grouper('abc')
        for g in (g_none, g_neg, g_zero, g_alpha):
            iterator = g('abcd')
            self.assertRaises(ValueError, list, iterator)

class DelimitedSplitterTests(TestCase):
    """Tests of the DelimitedSplitter factory function."""
    def test_parsers(self):
        """DelimitedSplitter should return function with correct behavior"""
        empty = DelimitedSplitter()
        space = DelimitedSplitter(None)
        semicolon = DelimitedSplitter(';')
        twosplits = DelimitedSplitter(';', 2)
        allsplits = DelimitedSplitter(';', None)
        lastone = DelimitedSplitter(';', -1)
        lasttwo = DelimitedSplitter(';', -2)

        self.assertEqual(empty('a   b  c'), ['a', 'b  c'])
        self.assertEqual(empty('abc'), ['abc'])
        self.assertEqual(empty('   '), [])

        self.assertEqual(empty('a  b  c'), space('a  b  c'))
        self.assertEqual(semicolon('  a  ; b   ;  c  d'), ['a','b   ;  c  d'])
        self.assertEqual(twosplits('  a  ; b   ;  c  d'), ['a','b', 'c  d'])
        self.assertEqual(allsplits(' a ;  b  ; c;;d;e  ;'),\
            ['a','b','c','','d','e',''])
        self.assertEqual(lastone(' a ;  b  ; c;;d;e  ;'),\
            ['a ;  b  ; c;;d;e',''])
        self.assertEqual(lasttwo(' a ;  b  ; c;;d;e  ;'),\
            ['a ;  b  ; c;;d','e',''])
        self.assertEqual(lasttwo(''), [])
        self.assertEqual(lasttwo('x'), ['x'])
        self.assertEqual(lasttwo('x;'), ['x', ''])

class GenericRecordTests(TestCase):
    """Tests of the GenericRecord class"""
    class gr(GenericRecord):
        Required = {'a':'x', 'b':[], 'c':{}}

    def test_init(self):
        """GenericRecord init should work OK empty or with data"""
        self.assertEqual(GenericRecord(), {})
        self.assertEqual(GenericRecord({'a':1}), {'a':1})
        assert isinstance(GenericRecord(), GenericRecord)

    def test_init_subclass(self):
        """GenericRecord subclass init should include required data"""
        self.assertEqual(self.gr(), {'a':'x', 'b':[], 'c':{}})
        self.assertEqual(self.gr({'a':[]}), {'a':[], 'b':[],'c':{}})
        assert isinstance(self.gr(), self.gr)
        assert isinstance(self.gr(), GenericRecord)

    def test_delitem(self):
        """GenericRecord delitem should fail if item required"""
        g = self.gr()
        g['d'] = 3
        self.assertEqual(g, {'a':'x','b':[],'c':{},'d':3})
        del g['d']
        self.assertEqual(g, {'a':'x','b':[],'c':{}})
        self.assertRaises(AttributeError, g.__delitem__, 'a')
        g['c'][3] = 4
        self.assertEqual(g['c'], {3:4})

    def test_copy(self):
        """GenericRecord copy should include attributes and set correct class"""
        g = self.gr()
        g['a'] = 'abc'
        g.X = 'y'
        h = g.copy()
        self.assertEqual(g, h)
        assert isinstance(h, self.gr)
        self.assertEqual(h.X, 'y')
        self.assertEqual(h, {'a':'abc', 'b':[], 'c':{}})
        
        

class MappedRecordTests(TestCase):
    """Tests of the MappedRecord class"""

    def setUp(self):
        """Define a few standard MappedRecords"""
        self.empty = MappedRecord()
        self.single = MappedRecord({'a':3})
        self.several = MappedRecord(a=4,b=5,c='a',d=[1,2,3])

    def test_init_empty(self):
        """MappedRecord empty init should work OK"""
        g = MappedRecord()
        self.assertEqual(g, {})

    def test_init_data(self):
        """MappedRecord should work like normal dict init"""
        exp = {'a':3, 'b':4}
        self.assertEqual(MappedRecord({'a':3, 'b':4}), exp)
        self.assertEqual(MappedRecord(a=3, b=4), exp)
        self.assertEqual(MappedRecord([['a',3],['b',4]]), exp)

    def test_init_subclass(self):
        """MappedRecord subclasses should behave as expected"""
        class rec(MappedRecord):
            Required = {'a':{}, 'b':'xyz', 'c':3}
            Aliases = {'B':'b'}

        r = rec()
        self.assertEqual(r, {'a':{}, 'b':'xyz', 'c':3})
        #test that subclassing is correct
        s = r.copy()
        assert isinstance(s, rec)
        #test Aliases
        s.B = 0
        self.assertEqual(s, {'a':{}, 'b':0, 'c':3})
        #test Required
        try:
            del s.B
        except AttributeError:
            pass
        else:
            raise AssertionError, "Subclass failed to catch requirement"

    def test_getattr(self):
        """MappedRecord getattr should look in dict after real attrs"""
        s = self.several
        self.assertEqual(s.Aliases, {})
        self.assertEqual(s.a, 4)
        self.assertEqual(s.d, [1,2,3])
        for key in s:
            self.assertEqual(getattr(s, key), s[key])
        assert 'xyz' not in s
        self.assertEqual(s.xyz, None)
        self.assertEqual(s['xyz'], None)
        s.Aliases = {'xyz':'a'}
        self.assertEqual(s['xyz'], 4)

    def test_setattr(self):
        """MappedRecord setattr should add to dict"""
        s = self.single
        #check that we haven't screwed up normal attribute setting
        assert 'Aliases' not in s
        s.Aliases = {'x':'y'}
        assert 'Aliases' not in s
        self.assertEqual(s.Aliases, {'x':'y'})
        s.x = 5
        assert 'x' in s
        self.assertEqual(s['x'], 5)
        self.assertEqual(s.x, 5)
        s.Aliases = {'XYZ':'b'}
        s.XYZ = 3
        self.assertEqual(s.b, 3)

    def test_delattr(self):
        """MappedRecord delattr should work for 'normal' and other attributes"""
        s = self.single
        s.__dict__['x'] = 'y'
        assert 'x' not in s
        self.assertEqual(s.x, 'y')
        del s.x
        self.assertEqual(s.x, None)
        self.assertEqual(s, {'a':3})
        #try it for an internal attribute: check it doesn't delete anything else
        s.b = 4
        self.assertEqual(s, {'a':3, 'b':4})
        del s.a
        self.assertEqual(s, {'b':4})
        del s.abc
        self.assertEqual(s, {'b':4})
        s.Required = {'b':True}
        try:
            del s.b
        except AttributeError:
            pass
        else:
            raise AssertionError, "Allowed deletion of required attribute"""
        s.a = 3
        self.assertEqual(s.a, 3)
        s.Aliases = {'xyz':'a'}
        del s.xyz
        self.assertEqual(s.a, None)

    def test_getitem(self):
        """MappedRecord getitem should work only for keys, not attributes"""
        s = self.single
        self.assertEqual(s['Required'], None)
        self.assertEqual(s['a'], 3)
        self.assertEqual(s['xyz'], None)
        self.assertEquals(s[list('abc')], None)
        s.Aliases = {'xyz':'a'}
        self.assertEqual(s['xyz'], 3)

    def test_setitem(self):
        """MappedRecord setitem should work only for keys, not attributes"""
        s = self.single
        s['Required'] = None
        self.assertEqual(s, {'a':3, 'Required':None})
        self.assertEqual(s.Required, {})
        self.assertNotEqual(s.Required, None)
        s['c'] = 5
        self.assertEqual(s, {'a':3, 'c':5, 'Required':None})
        #still not allowed unhashable objects as keys
        self.assertRaises(TypeError, s.__setitem__, range(3))
        s.Aliases = {'C':'c'}
        s['C'] = 3
        self.assertEqual(s, {'a':3, 'c':3, 'Required':None})
    
    def test_delitem(self):
        """MappedRecord delitem should only work for keys, not attributes"""
        s = self.single
        del s['Required']
        self.assertEqual(s.Required, {})
        s.Required = {'a':True}
        try:
            del s['a']
        except AttributeError:
            pass
        else:
            raise AssertionError, "Allowed deletion of required item"
        s.Aliases = {'B':'b'}
        s.b = 5
        self.assertEqual(s.b, 5)
        del s.B
        self.assertEqual(s.b, None)

    def test_contains(self):
        """MappedRecord contains should use aliases, but not apply to attrs"""
        s = self.single
        assert 'a' in s
        assert 'b' not in s
        s.b = 5
        assert 'b' in s
        assert 'Required' not in s
        assert 'A' not in s
        s.Aliases = {'A':'a'}
        assert 'A' in s

    def test_get(self):
        """MappedRecord get should be typesafe against unhashables"""
        s = self.single
        self.assertEqual(s.get(1, 6), 6)
        self.assertEqual(s.get('a', 'xyz'), 3)
        self.assertEqual(s.get('ABC', 'xyz'), 'xyz')
        s.Aliases = {'ABC':'a'}
        self.assertEqual(s.get('ABC', 'xyz'), 3)
        self.assertEqual(s.get([1,2,3], 'x'), 'x')

    def test_setdefault(self):
        """MappedRecord setdefault should not be typesafe against unhashables"""
        s = self.single
        x = s.setdefault('X', 'xyz')
        self.assertEqual(x, 'xyz')
        self.assertEqual(s, {'a':3, 'X':'xyz'})
        self.assertRaises(TypeError, s.setdefault, ['a','b'], 'xyz')

    def test_update(self):
        """MappedRecord update should transparently convert keys"""
        s = self.single
        s.b = 999
        s.Aliases = {'XYZ':'x', 'ABC':'a'}
        d = {'ABC':111, 'CVB':222}
        s.update(d)
        self.assertEqual(s, {'a':111, 'b':999, 'CVB':222})

    def test_copy(self):
        """MappedRecord copy should return correct class"""
        s = self.single
        t = s.copy()
        assert isinstance(t, MappedRecord)
        s.Aliases = {'XYZ':'x'}
        u = s.copy()
        u.Aliases['ABC'] = 'a'
        self.assertEqual(s.Aliases, {'XYZ':'x'})
        self.assertEqual(t.Aliases, {})
        self.assertEqual(u.Aliases, {'XYZ':'x', 'ABC':'a'})

    def test_subclass(self):
        """MappedRecord subclassing should work correctly"""
        class ret3(MappedRecord):
            DefaultValue = 3
            ClassData = 'xyz'

        x = ret3({'ABC':777, 'DEF':'999'})
        self.assertEqual(x.ZZZ, 3)
        self.assertEqual(x.ABC, 777)
        self.assertEqual(x.DEF, '999')
        self.assertEqual(x.ClassData, 'xyz')
        x.ZZZ = 6
        self.assertEqual(x.ZZZ, 6)
        self.assertEqual(x.ZZ, 3)
        x.ClassData = 'qwe'
        self.assertEqual(x.ClassData, 'qwe')
        self.assertEqual(ret3.ClassData, 'xyz')

    def test_DefaultValue(self):
        """MappedRecord DefaultValue should give new copy when requested"""
        class m(MappedRecord):
            DefaultValue=[]

        a = m()
        b = m()
        assert a['abc'] is not b['abc']
        assert a['abc'] == b['abc']
        
    
class dummy(object):
    """Do-nothing class whose attributes can be freely abused."""
    pass

class TypeSetterTests(TestCase):
    """Tests of the TypeSetter class"""
        
    def test_setter_empty(self):
        """TypeSetter should set attrs to vals on empty init"""
        d = dummy()
        ident = TypeSetter()
        ident(d, 'x', 'abc')
        self.assertEqual(d.x, 'abc')
        ident(d, 'y', 3)
        self.assertEqual(d.y, 3)
        ident(d, 'x', 2)
        self.assertEqual(d.x, 2)

    def test_setter_typed(self):
        """TypeSetter should set attrs to constructor(val) when specified"""
        d = dummy()
        i = TypeSetter(int)
        i(d, 'zz', 3)
        self.assertEqual(d.zz, 3)
        i(d, 'xx', '456')
        self.assertEqual(d.xx, 456)

class TypeSetterLikeTests(TestCase):
    """Tests of the functions that behave similarly to TypeSetter products"""
    
    def test_list_adder(self):
        """list_adder should add items to list, creating if necessary"""
        d = dummy()
        list_adder(d, 'x', 3)
        self.assertEqual(d.x, [3])
        list_adder(d, 'x', 'abc')
        self.assertEqual(d.x, [3, 'abc'])
        list_adder(d, 'y', [2,3])
        self.assertEqual(d.x, [3, 'abc'])
        self.assertEqual(d.y, [[2,3]])
    
    def test_list_extender(self):
        """list_adder should add items to list, creating if necessary"""
        d = dummy()
        list_extender(d, 'x', '345')
        self.assertEqual(d.x, ['3','4','5'])
        list_extender(d, 'x', 'abc')
        self.assertEqual(d.x, ['3','4','5','a','b','c'])
        list_extender(d, 'y', [2,3])
        self.assertEqual(d.x, ['3','4','5','a','b','c'])
        self.assertEqual(d.y, [2,3])
        list_extender(d, 'y', None)
        self.assertEqual(d.y, [2,3,None])
        
    def test_dict_adder(self):
        """dict_adder should add items to dict, creating if necessary"""
        d = dummy()
        dict_adder(d, 'x', 3)
        self.assertEqual(d.x, {3:None})
        dict_adder(d, 'x', 'ab')
        self.assertEqual(d.x, {3:None, 'a':'b'})
        dict_adder(d, 'x', ['a', 0])
        self.assertEqual(d.x, {3:None, 'a':0})
        dict_adder(d, 'y', None)
        self.assertEqual(d.x, {3:None, 'a':0})
        self.assertEqual(d.y, {None:None})

class LineOrientedConstructorTests(TestCase):
    """Tests of the LineOrientedConstructor class"""
    
    def test_init_empty(self):
        """LOC empty init should succeed with expected defaults"""
        l = LineOrientedConstructor()
        self.assertEqual(l.Lines, [])
        self.assertEqual(l.LabelSplitter(' ab  cd  '), ['ab','cd'])
        self.assertEqual(l.FieldMap, {})
        self.assertEqual(l.Constructor, MappedRecord)
        self.assertEqual(l.Strict, False)

    def test_empty_LOC(self):
        """LOC empty should fail if strict, fill fields if not strict"""
        data = ["abc   def","3  n","\t  abc   \txyz\n\n", "fgh   "]
        l = LineOrientedConstructor()
        result = l()
        self.assertEqual(result, {})
        result = l([])
        self.assertEqual(result, {})
        result = l(['   ','\n\t   '])
        self.assertEqual(result, {})
        result = l(data)
        self.assertEqual(result, {'abc':'xyz', '3':'n', 'fgh':None})
   
    def test_full_LOC(self):
        """LOC should behave as expected when initialized with rich data"""
        data = ["abc\t def"," 3 \t n","  abc   \txyz\n\n", "x\t5", "fgh   ", 
            "x\t3    "]
        class rec(MappedRecord):
            Required = {'abc':[]}
        maps = {'abc':list_adder, 'x':int_setter, 'fgh':bool_setter}
        label_splitter = DelimitedSplitter('\t')
        constructor = rec
        strict = True
        loc_bad = LineOrientedConstructor(data, label_splitter, maps, \
            constructor, strict)
        self.assertRaises(FieldError, loc_bad)
        strict = False
        loc_good = LineOrientedConstructor(data, label_splitter, maps, \
            constructor, strict)
        result = loc_good()
        assert isinstance(result, rec)
        self.assertEqual(result, \
            {'abc':['def','xyz'], '3':'n','fgh':False,'x':3})

class fake_dict(dict):
    """Test that constructors return the correct subclass"""
    pass

class FieldWrapperTests(TestCase):
    """Tests of the FieldWrapper factory function"""
    def test_default(self):
        """Default FieldWrapper should wrap fields and labels"""
        fields = list('abcde')
        f = FieldWrapper(fields)
        self.assertEqual(f(''), {})
        self.assertEqual(f('xy za '), {'a':'xy','b':'za'})
        self.assertEqual(f('1   2\t\t 3  \n4 5 6'), \
            {'a':'1','b':'2','c':'3','d':'4','e':'5'})

    def test_splitter(self):
        """FieldWrapper with splitter should use that splitter"""
        fields = ['label', 'count']
        splitter = DelimitedSplitter(':', -1)
        f = FieldWrapper(fields, splitter)
        self.assertEqual(f(''), {})
        self.assertEqual(f('nknasd:'), {'label':'nknasd', 'count':''})
        self.assertEqual(f('n:k:n:a:sd  '), {'label':'n:k:n:a', 'count':'sd'})

    def test_constructor(self):
        """FieldWrapper with constructor should use that constructor"""
        fields = list('abc')
        f = FieldWrapper(fields, constructor=fake_dict)
        self.assertEqual(f('x y'), {'a':'x','b':'y'})
        assert isinstance(f('x y'), fake_dict)
        
class StrictFieldWrapperTests(TestCase):
    """Tests of the StrictFieldWrapper factory function"""
    def test_default(self):
        """Default StrictFieldWrapper should wrap fields if count correct"""
        fields = list('abcde')
        f = StrictFieldWrapper(fields)
        self.assertEqual(f('1   2\t\t 3  \n4 5 '), \
            {'a':'1','b':'2','c':'3','d':'4','e':'5'})
        self.assertRaises(FieldError, f, '')
        self.assertRaises(FieldError, f, 'xy za ')

    def test_splitter(self):
        """StrictFieldWrapper with splitter should use that splitter"""
        fields = ['label', 'count']
        splitter = DelimitedSplitter(':', -1)
        f = StrictFieldWrapper(fields, splitter)
        self.assertEqual(f('n:k:n:a:sd  '), {'label':'n:k:n:a', 'count':'sd'})
        self.assertEqual(f('nknasd:'), {'label':'nknasd', 'count':''})
        self.assertRaises(FieldError, f, '')

    def test_constructor(self):
        """StrictFieldWrapper with constructor should use that constructor"""
        fields = list('ab')
        f = StrictFieldWrapper(fields, constructor=fake_dict)
        self.assertEqual(f('x y'), {'a':'x','b':'y'})
        assert isinstance(f('x y'), fake_dict)
        
class FieldMorpherTests(TestCase):
    """Tests of the FieldMorpher class."""
    def test_default(self):
        """FieldMorpher default should use correct constructors"""
        fm = FieldMorpher({'a':int, 'b':str})
        self.assertEqual(fm({'a':'3', 'b':456}), {'a':3,'b':'456'})
        
    def test_default_error(self):
        """FieldMorpher default should raise FieldError on unknown fields"""
        fm = FieldMorpher({'a':int, 'b':str})
        self.assertRaises(FieldError, fm, {'a':'3', 'b':456, 'c':'4'})

    def test_altered_default(self):
        """FieldMorpher with default set should apply it"""
        func = lambda x, y: (str(x), float(y) - 0.5)
        fm = FieldMorpher({'3':str,4:int}, func)
        #check that recognized values aren't tampered with
        self.assertEqual(fm({3:3, 4:'4'}), {'3':'3', 4:4})
        #check that unrecognized values get the appropriate conversion
        self.assertEqual(fm({3:3, 5:'5'}), {'3':'3', '5':4.5})
        
if __name__ == '__main__':
    main()
