# Copyright (c) 2011-2020 Eric Froemling # # 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, sublicense, 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. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ----------------------------------------------------------------------------- """Testing entity functionality.""" from __future__ import annotations from typing import TYPE_CHECKING from enum import Enum, unique import pytest from efro import entity from efrotools.statictest import static_type_equals if TYPE_CHECKING: pass @unique class EnumTest(Enum): """Testing...""" FIRST = 0 SECOND = 1 class SubCompoundTest(entity.CompoundValue): """Testing...""" subval = entity.Field('b', entity.BoolValue()) class CompoundTest(entity.CompoundValue): """Testing...""" isubval = entity.Field('i', entity.IntValue(default=34532)) compoundlist = entity.CompoundListField('l', SubCompoundTest()) class CompoundTest2(CompoundTest): """Testing...""" isubval2 = entity.Field('i2', entity.IntValue(default=3453)) class EntityTest(entity.Entity): """Testing...""" ival = entity.Field('i', entity.IntValue(default=345)) sval = entity.Field('s', entity.StringValue(default='svvv')) bval = entity.Field('b', entity.BoolValue(default=True)) fval = entity.Field('f', entity.FloatValue(default=1.0)) grp = entity.CompoundField('g', CompoundTest()) grp2 = entity.CompoundField('g2', CompoundTest2()) enumval = entity.Field('e', entity.EnumValue(EnumTest, default=None)) enumval2 = entity.Field( 'e2', entity.OptionalEnumValue(EnumTest, default=EnumTest.SECOND)) slval = entity.ListField('sl', entity.StringValue()) tval2 = entity.Field('t2', entity.DateTimeValue()) str_int_dict = entity.DictField('sd', str, entity.IntValue()) compoundlist = entity.CompoundListField('l', CompoundTest()) compoundlist2 = entity.CompoundListField('l2', CompoundTest()) compoundlist3 = entity.CompoundListField('l3', CompoundTest2()) compounddict = entity.CompoundDictField('td', str, CompoundTest()) compounddict2 = entity.CompoundDictField('td2', str, CompoundTest()) compounddict3 = entity.CompoundDictField('td3', str, CompoundTest2()) fval2 = entity.Field('f2', entity.Float3Value()) # noinspection PyTypeHints def test_entity_values() -> None: """Test various entity assigns for value and type correctness.""" ent = EntityTest() # Simple int field. with pytest.raises(TypeError): ent.ival = 'strval' # type: ignore assert static_type_equals(ent.ival, int) assert isinstance(ent.ival, int) assert ent.ival == 345 ent.ival = 346 assert ent.ival == 346 # Simple float field. with pytest.raises(TypeError): ent.fval = "foo" # type: ignore assert static_type_equals(ent.fval, float) ent.fval = 2 ent.fval = True ent.fval = 1.0 # Simple value list field. assert not ent.slval assert len(ent.slval) == 0 with pytest.raises(TypeError): ent.slval.append(1) # type: ignore ent.slval.append('blah') assert len(ent.slval) == 1 assert list(ent.slval) == ['blah'] with pytest.raises(TypeError): ent.slval = ['foo', 'bar', 1] # type: ignore # Simple value dict field. assert 'foo' not in ent.str_int_dict # Set with incorrect key type should give TypeError. with pytest.raises(TypeError): ent.str_int_dict[0] = 123 # type: ignore # And set with incorrect value type should do same. with pytest.raises(TypeError): ent.str_int_dict['foo'] = 'bar' # type: ignore ent.str_int_dict['foo'] = 123 assert static_type_equals(ent.str_int_dict['foo'], int) assert ent.str_int_dict['foo'] == 123 # Waaah; this works at runtime, but it seems that we'd need # to have BoundDictField inherit from Mapping for mypy to accept this. # (which seems to get a bit ugly, but may be worth revisiting) # assert dict(ent.str_int_dict) == {'foo': 123} # Passing key/value pairs as a list works though.. assert dict(ent.str_int_dict.items()) == {'foo': 123} # noinspection PyTypeHints def test_entity_values_2() -> None: """Test various entity assigns for value and type correctness.""" ent = EntityTest() # Compound value assert static_type_equals(ent.grp, CompoundTest) assert static_type_equals(ent.grp.isubval, int) assert isinstance(ent.grp.isubval, int) with pytest.raises(TypeError): ent.grp.isubval = 'blah' # type: ignore # Compound value inheritance. assert ent.grp2.isubval2 == 3453 assert ent.grp2.isubval == 34532 # Compound list field. with pytest.raises(IndexError): print(ent.compoundlist[0]) with pytest.raises(TypeError): ent.compoundlist[0] = 123 # type: ignore assert len(ent.compoundlist) == 0 assert not ent.compoundlist ent.compoundlist.append() assert ent.compoundlist assert len(ent.compoundlist) == 1 assert static_type_equals(ent.compoundlist[0], CompoundTest) # Compound dict field. cdval = ent.compounddict.add('foo') assert static_type_equals(cdval, CompoundTest) # Set with incorrect key type should give TypeError. with pytest.raises(TypeError): _cdval2 = ent.compounddict.add(1) # type: ignore # Hmm; should this throw a TypeError and not a KeyError?.. with pytest.raises(KeyError): _cdval3 = ent.compounddict[1] # type: ignore assert static_type_equals(ent.compounddict['foo'], CompoundTest) # Enum value with pytest.raises(ValueError): ent.enumval = None # type: ignore assert ent.enumval == EnumTest.FIRST # Optional Enum value ent.enumval2 = None assert ent.enumval2 is None # Nested compound values assert not ent.grp.compoundlist val = ent.grp.compoundlist.append() assert static_type_equals(val, SubCompoundTest) assert static_type_equals(ent.grp.compoundlist[0], SubCompoundTest) assert static_type_equals(ent.grp.compoundlist[0].subval, bool) # noinspection PyTypeHints def test_field_copies() -> None: """Test copying various values between fields.""" ent1 = EntityTest() ent2 = EntityTest() # Copying a simple value. ent1.ival = 334 ent2.ival = ent1.ival assert ent2.ival == 334 # Copying a nested compound. ent1.grp.isubval = 543 ent2.grp = ent1.grp assert ent2.grp.isubval == 543 # Type-checker currently allows this because both are Compounds # but should fail at runtime since their subfield arrangement differs. # reveal_type(ent1.grp.blah) with pytest.raises(ValueError): ent2.grp = ent1.grp2 # Copying a value list. ent1.slval = ['foo', 'bar'] assert ent1.slval == ['foo', 'bar'] ent2.slval = ent1.slval assert ent2.slval == ['foo', 'bar'] # Copying a value dict. ent1.str_int_dict['tval'] = 987 ent2.str_int_dict = ent1.str_int_dict assert ent2.str_int_dict['tval'] == 987 # Copying a CompoundList val = ent1.compoundlist.append() val.isubval = 356 assert ent1.compoundlist[0].isubval == 356 assert len(ent1.compoundlist) == 1 ent1.compoundlist.append() assert len(ent1.compoundlist) == 2 assert len(ent2.compoundlist) == 0 # Copying to the same field on different obj should work. ent2.compoundlist = ent1.compoundlist assert ent2.compoundlist[0].isubval == 356 assert len(ent2.compoundlist) == 2 # Cross-field assigns should work too if the field layouts match.. ent1.compoundlist2 = ent1.compoundlist # And not if they don't... # (in this case mypy errors too but that may not always be the case) with pytest.raises(ValueError): ent1.compoundlist3 = ent1.compoundlist # type: ignore # Copying a CompoundDict ent1.compounddict.add('foo') ent1.compounddict.add('bar') assert static_type_equals(ent1.compounddict['foo'].isubval, int) ent1.compounddict['foo'].isubval = 23 # Copying to the same field on different obj should work. ent2.compounddict = ent1.compounddict assert ent2.compounddict.keys() == ['foo', 'bar'] assert ent2.compounddict['foo'].isubval == 23 # Cross field assigns should work too if the field layouts match.. ent1.compounddict2 = ent1.compounddict # ..And should fail otherwise. # (mypy catches this too, but that may not always be the case if # two CompoundValues have the same type but different layouts based # on their __init__ args or whatnot) with pytest.raises(ValueError): ent1.compounddict3 = ent1.compounddict # type: ignore # Make sure invalid key types get caught when setting a full dict: with pytest.raises(TypeError): ent1.compounddict2 = { 'foo': ent1.compounddict['foo'], 2: ent1.compounddict['bar'], # type: ignore } def test_field_access_from_type() -> None: """Accessing fields through type objects should return the Field objs.""" ent = EntityTest() # Accessing fields through the type should return field objects # instead of values. assert static_type_equals(ent.ival, int) assert isinstance(ent.ival, int) mypytype = 'efro.entity._field.Field[builtins.int*]' assert static_type_equals(type(ent).ival, mypytype) assert isinstance(type(ent).ival, entity.Field) # Accessing subtype on a nested compound field.. assert static_type_equals(type(ent).compoundlist.d_value, CompoundTest) assert isinstance(type(ent).compoundlist.d_value, CompoundTest) class EntityTestMixin(entity.EntityMixin, CompoundTest2): """A test entity created from a compound using a mixin class.""" def test_entity_mixin() -> None: """Testing our mixin entity variety.""" ent = EntityTestMixin() assert static_type_equals(ent.isubval2, int) assert ent.isubval2 == 3453 def test_entity_embedding() -> None: """Making sure compound entities work as expected.""" class EmbCompoundValTest(entity.CompoundValue): """Testing...""" isubval = entity.Field('i', entity.IntValue(default=12345)) class EmbCompoundTest(entity.Entity): """Testing...""" isubval = entity.Field('i', entity.IntValue(default=12345)) sub = entity.CompoundField('sub', EmbCompoundValTest()) # This should be ok... _ent = EmbCompoundTest() class EmbCompoundValTest2(entity.Entity): """Testing...""" isubval = entity.Field('i', entity.IntValue(default=12345)) with pytest.raises(AssertionError): # This should not be ok # (can only embed CompoundValues, not complete Entities) class EmbCompoundTest2(entity.Entity): """Testing...""" isubval = entity.Field('i', entity.IntValue(default=12345)) sub = entity.CompoundField('sub', EmbCompoundValTest2()) _ent2 = EmbCompoundTest2() def test_key_uniqueness() -> None: """Make sure entities reject multiple fields with the same key.""" # Make sure a single entity with dup keys fails: with pytest.raises(RuntimeError): class EntityKeyTest(entity.Entity): """Test entity with invalid duplicate keys.""" ival = entity.Field('i', entity.IntValue()) sval = entity.Field('i', entity.StringValue()) _ent = EntityKeyTest() # Make sure we still get an error if the duplicate keys come from # different places in the class hierarchy. with pytest.raises(RuntimeError): class EntityKeyTest2(entity.Entity): """Test entity with invalid duplicate keys.""" ival = entity.Field('i', entity.IntValue()) class EntityKeyTest3(EntityKeyTest2): """Test entity with invalid duplicate keys.""" sval = entity.Field('i', entity.StringValue()) _ent2 = EntityKeyTest3() def test_data_storage_and_fetching() -> None: """Test store_default option for entities.""" class EntityTestD(entity.Entity): """Testing store_default off.""" ival = entity.Field('i', entity.IntValue(default=3, store_default=False)) class EntityTestD2(entity.Entity): """Testing store_default on (the default).""" ival = entity.Field('i', entity.IntValue(default=3)) # This guy should get pruned when its got a default value. testd = EntityTestD() assert testd.ival == 3 assert testd.pruned_data() == {} testd.ival = 4 assert testd.pruned_data() == {'i': 4} testd.ival = 3 assert testd.pruned_data() == {} # Make sure our pretty/prune json options work. assert testd.to_json_str() == '{}' assert testd.to_json_str(prune=False) == '{"i":3}' assert testd.to_json_str(prune=False, pretty=True) == ('{\n' ' "i": 3\n' '}') # This guy should never get pruned... testd2 = EntityTestD2() assert testd2.ival == 3 assert testd2.pruned_data() == {'i': 3} testd2.ival = 4 assert testd2.pruned_data() == {'i': 4} testd2.ival = 3 assert testd2.to_json_str(prune=True) == '{"i":3}'