From 355d9758035cf5853385d7f6ba566dbf4af10867 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Sat, 21 Dec 2019 13:39:44 -0800 Subject: [PATCH] latest entity work --- .idea/dictionaries/ericf.xml | 4 + .../scripts/bafoundation/entity/_field.py | 89 ++++++++++++++++--- docs/ba_module.md | 4 +- tests/test_bafoundation/test_entities.py | 58 +++++++++++- tools/efrotools/snippets.py | 15 +++- 5 files changed, 150 insertions(+), 20 deletions(-) diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 34af5de3..b199ff6b 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -219,6 +219,7 @@ ccompiler cdrk cdull + cdval centeuro centiseconds cfconfig @@ -287,6 +288,7 @@ compat compileall compilelocations + compounddict compoundlist configerror confighash @@ -518,6 +520,7 @@ farthestpt fbase fclose + fcmd fcntlmodule fcode fcontents @@ -591,6 +594,7 @@ fpathrel fpathshort fprint + fproject fpsc framerate freefly diff --git a/assets/src/data/scripts/bafoundation/entity/_field.py b/assets/src/data/scripts/bafoundation/entity/_field.py index 2fbfa7ac..c269e40d 100644 --- a/assets/src/data/scripts/bafoundation/entity/_field.py +++ b/assets/src/data/scripts/bafoundation/entity/_field.py @@ -30,6 +30,7 @@ from bafoundation.entity._support import (BaseField, BoundCompoundValue, BoundListField, BoundDictField, BoundCompoundListField, BoundCompoundDictField) +from bafoundation.entity.util import have_matching_fields if TYPE_CHECKING: from typing import Dict, Type, List, Any @@ -203,6 +204,7 @@ class ListField(BaseField, Generic[T]): # When accessed on a FieldInspector we return a sub-field FieldInspector. # When accessed on an instance we return a BoundListField. + # noinspection DuplicatedCode if TYPE_CHECKING: # Access via type gives our field; via an instance gives a bound field. @@ -277,6 +279,7 @@ class DictField(BaseField, Generic[TK, T]): # change the dict, but we can prune completely if empty (and allowed) return not data and not self._store_default + # noinspection DuplicatedCode if TYPE_CHECKING: # Return our field if accessed via type and bound-dict-field @@ -354,6 +357,7 @@ class CompoundListField(BaseField, Generic[TC]): # We can also optionally prune the whole list if empty and allowed. return not data and not self._store_default + # noinspection DuplicatedCode if TYPE_CHECKING: @overload @@ -370,10 +374,10 @@ class CompoundListField(BaseField, Generic[TC]): ... # Note: - # When setting the list, we tell the type-checker that we accept + # When setting the list, we tell the type-checker that we also accept # a raw list of CompoundValue objects, but at runtime we actually # always deal with BoundCompoundValue objects (see note in - # BoundCompoundListField) + # BoundCompoundListField for why we accept CompoundValue objs) @overload def __set__(self, obj: Any, value: List[TC]) -> None: ... @@ -392,7 +396,7 @@ class CompoundListField(BaseField, Generic[TC]): def set_with_data(self, data: Any, value: Any, error: bool) -> Any: # If we were passed a BoundCompoundListField, - # simply convert it to a list of BoundCompoundValue objects which + # simply convert it to a flat list of BoundCompoundValue objects which # is what we work with natively here. if isinstance(value, BoundCompoundListField): value = list(value) @@ -406,10 +410,28 @@ class CompoundListField(BaseField, Generic[TC]): # be sure the underlying data will line up; for example two # CompoundListFields with different child_field values should not # be inter-assignable. - if (not all(isinstance(i, BoundCompoundValue) for i in value) - or not all(i.d_value is self.d_value for i in value)): + if not all(isinstance(i, BoundCompoundValue) for i in value): raise ValueError('CompoundListField assignment must be a ' - 'list containing only its existing children.') + 'list containing only BoundCompoundValue objs.') + + # Make sure the data all has the same CompoundValue type and + # compare that type against ours once to make sure its fields match. + # (this will not allow passing CompoundValues from multiple sources + # but I don't know if that would ever come up..) + for i, val in enumerate(value): + if i == 0: + # Do the full field comparison on the first value only.. + if not have_matching_fields(val.d_value, self.d_value): + raise ValueError( + 'CompoundListField assignment must be a ' + 'list containing matching CompoundValues.') + else: + # For all remaining values, just ensure they match the first. + if val.d_value is not value[0].d_value: + raise ValueError( + 'CompoundListField assignment cannot contain ' + 'multiple CompoundValue types as sources.') + data[self.d_key] = self.filter_input([i.d_data for i in value], error=error) @@ -468,6 +490,7 @@ class CompoundDictField(BaseField, Generic[TK, TC]): # ONLY overriding these in type-checker land to clarify types. # (see note in BaseField) + # noinspection DuplicatedCode if TYPE_CHECKING: @overload @@ -485,14 +508,35 @@ class CompoundDictField(BaseField, Generic[TK, TC]): def __get__(self, obj: Any, cls: Any = None) -> Any: ... + # Note: + # When setting the dict, we tell the type-checker that we also accept + # a raw dict of CompoundValue objects, but at runtime we actually + # always deal with BoundCompoundValue objects (see note in + # BoundCompoundDictField for why we accept CompoundValue objs) + @overload def __set__(self, obj: Any, value: Dict[TK, TC]) -> None: ... + @overload + def __set__(self, obj: Any, value: BoundCompoundDictField[TK, + TC]) -> None: + ... + + def __set__(self, obj: Any, value: Any) -> None: + ... + def get_with_data(self, data: Any) -> Any: assert self.d_key in data return BoundCompoundDictField(self, data[self.d_key]) def set_with_data(self, data: Any, value: Any, error: bool) -> Any: + + # If we were passed a BoundCompoundDictField, + # simply convert it to a flat dict of BoundCompoundValue objects which + # is what we work with natively here. + if isinstance(value, BoundCompoundDictField): + value = dict(value.items()) + if not isinstance(value, dict): raise TypeError('CompoundDictField expected dict value on set.') @@ -501,10 +545,31 @@ class CompoundDictField(BaseField, Generic[TK, TC]): # be sure the underlying data will line up; for example two # CompoundListFields with different child_field values should not # be inter-assignable. - if (not all(isinstance(i, self.d_keytype) for i in value.keys()) - or not all( - isinstance(i, BoundCompoundValue) for i in value.values()) - or not all(i.d_value is self.d_value for i in value.values())): + if (not all(isinstance(i, BoundCompoundValue) + for i in value.values())): raise ValueError('CompoundDictField assignment must be a ' - 'dict containing only its existing children.') - data[self.d_key] = {key: val.d_data for key, val in value.items()} + 'dict containing only BoundCompoundValues.') + + # Make sure the data all has the same CompoundValue type and + # compare that type against ours once to make sure its fields match. + # (this will not allow passing CompoundValues from multiple sources + # but I don't know if that would ever come up..) + first_value: Any = None + for i, val in enumerate(value.values()): + if i == 0: + first_value = val.d_value + # Do the full field comparison on the first value only.. + if not have_matching_fields(val.d_value, self.d_value): + raise ValueError( + 'CompoundListField assignment must be a ' + 'list containing matching CompoundValues.') + else: + # For all remaining values, just ensure they match the first. + if val.d_value is not first_value: + raise ValueError( + 'CompoundListField assignment cannot contain ' + 'multiple CompoundValue types as sources.') + + data[self.d_key] = self.filter_input( + {key: val.d_data + for key, val in value.items()}, error=error) diff --git a/docs/ba_module.md b/docs/ba_module.md index e1bc0a58..9aea2fe1 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,6 +1,6 @@ - -

last updated on 2019-12-19 for Ballistica version 1.5.0 build 20001

+ +

last updated on 2019-12-21 for Ballistica version 1.5.0 build 20001

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/tests/test_bafoundation/test_entities.py b/tests/test_bafoundation/test_entities.py index cceaf700..a97eeec8 100644 --- a/tests/test_bafoundation/test_entities.py +++ b/tests/test_bafoundation/test_entities.py @@ -72,7 +72,11 @@ class EntityTest(entity.Entity): tval2 = entity.Field('t2', entity.DateTimeValue()) str_int_dict = entity.DictField('sd', str, entity.IntValue()) compoundlist = entity.CompoundListField('l', CompoundTest()) - tdval = entity.CompoundDictField('td', str, 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()) @@ -111,8 +115,10 @@ def test_entity_values() -> None: # 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 @@ -124,6 +130,9 @@ def test_entity_values() -> None: # (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: @@ -154,6 +163,17 @@ def test_entity_values_2() -> None: 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 @@ -171,6 +191,7 @@ def test_entity_values_2() -> None: 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() @@ -208,10 +229,43 @@ def test_field_copies() -> None: 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) == 1 + 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: diff --git a/tools/efrotools/snippets.py b/tools/efrotools/snippets.py index 7e255db1..12f95b1c 100644 --- a/tools/efrotools/snippets.py +++ b/tools/efrotools/snippets.py @@ -357,19 +357,26 @@ def sync_all() -> None: This assumes that there is a 'sync-full' and 'sync-list' Makefile target under each project. """ + import concurrent.futures print(f'{CLRBLU}Updating formatting for all projects...{CLREND}') projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS') if projects_str is None: raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.') + projects = projects_str.split(':') + + def _format_project(fproject: str) -> None: + fcmd = f'cd "{fproject}" && make format' + print(fcmd) + subprocess.run(fcmd, shell=True, check=True) # No matter what we're doing (even if just listing), run formatting # in all projects before beginning. Otherwise if we do a sync and then # a preflight we'll often wind up getting out-of-sync errors due to # formatting changing after the sync. - for project in projects_str.split(':'): - cmd = f'cd "{project}" && make format' - print(cmd) - subprocess.run(cmd, shell=True, check=True) + with concurrent.futures.ThreadPoolExecutor( + max_workers=len(projects)) as executor: + # Converting this to a list will propagate any errors. + list(executor.map(_format_project, projects)) if len(sys.argv) > 2 and sys.argv[2] == 'list': # List mode