diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 91c91dd1..12147f94 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1605,6 +1605,7 @@ testcapi testcapimodule testclass + testd testfoo testfooooo testhelpers diff --git a/Makefile b/Makefile index 9cc1407d..358a0779 100644 --- a/Makefile +++ b/Makefile @@ -330,7 +330,7 @@ pycharmfull: prereqs # Note: need to disable bytecode writing so we don't cause errors due to # unexpected __pycache__ dirs popping up. test: prereqs - @tools/snippets pytest tests + @tools/snippets pytest -v tests ################################################################################ diff --git a/assets/src/data/scripts/bafoundation/entity/_entity.py b/assets/src/data/scripts/bafoundation/entity/_entity.py index db590c0e..7af497fb 100644 --- a/assets/src/data/scripts/bafoundation/entity/_entity.py +++ b/assets/src/data/scripts/bafoundation/entity/_entity.py @@ -121,11 +121,11 @@ class EntityMixin: self.d_data = target.d_data target.d_data = None - def get_pruned_data(self) -> Dict[str, Any]: + def pruned_data(self) -> Dict[str, Any]: """Return a pruned version of this instance's data. This varies from d_data in that values may be stripped out if - they are equal to defaults (if the field allows such). + they are equal to defaults (for fields with that option enabled). """ import copy data = copy.deepcopy(self.d_data) @@ -133,24 +133,26 @@ class EntityMixin: self.prune_fields_data(data) return data - def to_json_str(self, pretty: bool = False) -> str: + def to_json_str(self, prune: bool = True, pretty: bool = False) -> str: """Convert the entity to a json string. This uses bafoundation.jsontools.ExtendedJSONEncoder/Decoder to support data types not natively storable in json. """ + if prune: + data = self.pruned_data() + else: + data = self.d_data if pretty: - return json.dumps(self.d_data, + return json.dumps(data, indent=2, sort_keys=True, cls=ExtendedJSONEncoder) - return json.dumps(self.d_data, - separators=(',', ':'), - cls=ExtendedJSONEncoder) + return json.dumps(data, separators=(',', ':'), cls=ExtendedJSONEncoder) @staticmethod def json_loads(s: str) -> Any: - """Load a json string with our special extended decoder. + """Load a json string using our special extended decoder. Note that this simply returns loaded json data; no Entities are involved. diff --git a/assets/src/data/scripts/bafoundation/entity/_field.py b/assets/src/data/scripts/bafoundation/entity/_field.py index 8c8c0a87..3b0236b5 100644 --- a/assets/src/data/scripts/bafoundation/entity/_field.py +++ b/assets/src/data/scripts/bafoundation/entity/_field.py @@ -47,7 +47,7 @@ class Field(BaseField, Generic[T]): def __init__(self, d_key: str, value: 'TypedValue[T]', - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) self.d_value = value self._store_default = store_default @@ -86,7 +86,7 @@ class CompoundField(BaseField, Generic[TC]): def __init__(self, d_key: str, value: TC, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) if __debug__ is True: from bafoundation.entity._value import CompoundValue @@ -165,7 +165,7 @@ class ListField(BaseField, Generic[T]): def __init__(self, d_key: str, value: 'TypedValue[T]', - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) self.d_value = value self._store_default = store_default @@ -218,7 +218,7 @@ class DictField(BaseField, Generic[TK, T]): d_key: str, keytype: Type[TK], field: 'TypedValue[T]', - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) self.d_value = field self._store_default = store_default @@ -280,7 +280,7 @@ class CompoundListField(BaseField, Generic[TC]): def __init__(self, d_key: str, valuetype: TC, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) self.d_value = valuetype @@ -369,7 +369,7 @@ class CompoundDictField(BaseField, Generic[TK, TC]): d_key: str, keytype: Type[TK], valuetype: TC, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(d_key) self.d_value = valuetype diff --git a/assets/src/data/scripts/bafoundation/entity/_value.py b/assets/src/data/scripts/bafoundation/entity/_value.py index acc5a0b8..8c7499ef 100644 --- a/assets/src/data/scripts/bafoundation/entity/_value.py +++ b/assets/src/data/scripts/bafoundation/entity/_value.py @@ -130,7 +130,7 @@ class SimpleValue(TypedValue[T]): class StringValue(SimpleValue[str]): """Value consisting of a single string.""" - def __init__(self, default: str = "", store_default: bool = False) -> None: + def __init__(self, default: str = "", store_default: bool = True) -> None: super().__init__(default, store_default, str) @@ -139,7 +139,7 @@ class OptionalStringValue(SimpleValue[Optional[str]]): def __init__(self, default: Optional[str] = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, str, allow_none=True) @@ -148,7 +148,7 @@ class BoolValue(SimpleValue[bool]): def __init__(self, default: bool = False, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, bool, (int, float)) @@ -157,7 +157,7 @@ class OptionalBoolValue(SimpleValue[Optional[bool]]): def __init__(self, default: Optional[bool] = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, bool, (int, float), @@ -207,7 +207,7 @@ class DateTimeValue(SimpleValue[datetime.datetime]): The default value for this is always the current time in UTC. """ - def __init__(self, store_default: bool = False) -> None: + def __init__(self, store_default: bool = True) -> None: # Pass dummy datetime value as default just to satisfy constructor; # we override get_default_data though so this doesn't get used. dummy_default = datetime.datetime.now(datetime.timezone.utc) @@ -226,7 +226,7 @@ class DateTimeValue(SimpleValue[datetime.datetime]): class OptionalDateTimeValue(SimpleValue[Optional[datetime.datetime]]): """Value consisting of a datetime.datetime object or None.""" - def __init__(self, store_default: bool = False) -> None: + def __init__(self, store_default: bool = True) -> None: super().__init__(None, store_default, datetime.datetime, @@ -240,7 +240,7 @@ class OptionalDateTimeValue(SimpleValue[Optional[datetime.datetime]]): class IntValue(SimpleValue[int]): """Value consisting of a single int.""" - def __init__(self, default: int = 0, store_default: bool = False) -> None: + def __init__(self, default: int = 0, store_default: bool = True) -> None: super().__init__(default, store_default, int, (bool, float)) @@ -249,7 +249,7 @@ class OptionalIntValue(SimpleValue[Optional[int]]): def __init__(self, default: int = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, int, (bool, float), @@ -261,7 +261,7 @@ class FloatValue(SimpleValue[float]): def __init__(self, default: float = 0.0, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, float, (bool, int)) @@ -270,7 +270,7 @@ class OptionalFloatValue(SimpleValue[Optional[float]]): def __init__(self, default: float = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default, float, (bool, int), @@ -282,7 +282,7 @@ class Float3Value(SimpleValue[Tuple[float, float, float]]): def __init__(self, default: Tuple[float, float, float] = (0.0, 0.0, 0.0), - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(default, store_default) def __repr__(self) -> str: @@ -315,7 +315,7 @@ class BaseEnumValue(TypedValue[T]): def __init__(self, enumtype: Type[T], default: Optional[T] = None, - store_default: bool = False, + store_default: bool = True, allow_none: bool = False) -> None: super().__init__() assert issubclass(enumtype, Enum) @@ -388,7 +388,7 @@ class EnumValue(BaseEnumValue[TE]): def __init__(self, enumtype: Type[TE], default: TE = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(enumtype, default, store_default, allow_none=False) @@ -401,7 +401,7 @@ class OptionalEnumValue(BaseEnumValue[Optional[TE]]): def __init__(self, enumtype: Type[TE], default: TE = None, - store_default: bool = False) -> None: + store_default: bool = True) -> None: super().__init__(enumtype, default, store_default, allow_none=True) @@ -412,7 +412,7 @@ class CompoundValue(DataHandler): any number of Field instances within themself. """ - def __init__(self, store_default: bool = False) -> None: + def __init__(self, store_default: bool = True) -> None: super().__init__() self._store_default = store_default diff --git a/docs/ba_module.md b/docs/ba_module.md index 11295957..d9a401c3 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,6 +1,6 @@ - -

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

+ +

last updated on 2019-12-14 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 43b1ca3e..dd0436a5 100644 --- a/tests/test_bafoundation/test_entities.py +++ b/tests/test_bafoundation/test_entities.py @@ -18,7 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ----------------------------------------------------------------------------- -"""Testing tests.""" +"""Testing entity functionality.""" from __future__ import annotations @@ -34,7 +34,6 @@ if TYPE_CHECKING: pass -# A smattering of enum value types... @unique class EnumTest(Enum): """Testing...""" @@ -42,9 +41,15 @@ class EnumTest(Enum): 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): @@ -58,8 +63,8 @@ class EntityTest(entity.Entity): 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', CompoundTest2()) - grp2 = entity.CompoundField('g2', CompoundTest()) + 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)) @@ -71,17 +76,13 @@ class EntityTest(entity.Entity): fval2 = entity.Field('f2', entity.Float3Value()) -class EntityTest2(entity.EntityMixin, CompoundTest2): - """test.""" - - +# 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): - # noinspection PyTypeHints ent.ival = 'strval' # type: ignore assert static_type_equals(ent.ival, int) assert isinstance(ent.ival, int) @@ -89,23 +90,32 @@ def test_entity_values() -> None: 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 str/int dict field. assert 'foo' not in ent.str_int_dict with pytest.raises(TypeError): - # noinspection PyTypeHints ent.str_int_dict[0] = 123 # type: ignore with pytest.raises(TypeError): - # noinspection PyTypeHints 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 + # 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): - # noinspection PyTypeHints ent.compoundlist[0] = 123 # type: ignore assert len(ent.compoundlist) == 0 assert not ent.compoundlist @@ -113,3 +123,95 @@ def test_entity_values() -> None: assert ent.compoundlist assert len(ent.compoundlist) == 1 assert static_type_equals(ent.compoundlist[0], 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) + + +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_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}' diff --git a/tools/cloudtool b/tools/cloudtool index b24526eb..413e3674 100755 --- a/tools/cloudtool +++ b/tools/cloudtool @@ -65,9 +65,9 @@ CMD_LOGOUT = 'logout' CMD_PUTASSETPACK = 'putassetpack' CMD_HELP = 'help' +# Note to self: keep this synced with server-side logic. ASSET_PACKAGE_NAME_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' ASSET_PACKAGE_NAME_MAX_LENGTH = 32 - ASSET_PATH_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' ASSET_PATH_MAX_LENGTH = 128 diff --git a/tools/efrotools/statictest.py b/tools/efrotools/statictest.py index 1d9afc4b..b23499bb 100644 --- a/tools/efrotools/statictest.py +++ b/tools/efrotools/statictest.py @@ -185,12 +185,16 @@ def static_type_equals(value: Any, statictype: Type) -> bool: wanttype = testfile.linetypes_wanted[linenumber] mypytype = testfile.linetypes_mypy[linenumber] - # Do some filtering of Mypy types to simple python ones. + # Do some filtering of Mypy types so we can compare to simple python ones. # (ie: 'builtins.list[builtins.int*]' -> int) + # Note to self: perhaps we'd want a fallback form where we can pass a + # type as a string if we want to match the exact mypy value?... mypytype = mypytype.replace('*', '') mypytype = mypytype.replace('?', '') mypytype = mypytype.replace('builtins.int', 'int') + mypytype = mypytype.replace('builtins.float', 'float') mypytype = mypytype.replace('builtins.list', 'List') + mypytype = mypytype.replace('builtins.bool', 'bool') mypytype = mypytype.replace('typing.Sequence', 'Sequence') # temp3.FooClass -> FooClass diff --git a/tools/update_project b/tools/update_project index f25c9d6e..fa066402 100755 --- a/tools/update_project +++ b/tools/update_project @@ -443,11 +443,14 @@ class App: # Check our packages and make sure all subdirs contain and __init__.py # (I tend to forget this sometimes) packagedirs = ['tools/efrotools'] - script_asset_dir = 'assets/src/data/scripts' - for name in os.listdir(script_asset_dir): - # (Assume all dirs under our script assets dir are packages) - if os.path.isdir(os.path.join(script_asset_dir)): - packagedirs.append(os.path.join(script_asset_dir, name)) + + # (Assume all dirs under these dirs are packages) + dirs_of_packages = ['assets/src/data/scripts', 'tests'] + for dir_of_packages in dirs_of_packages: + for name in os.listdir(dir_of_packages): + if (not name.startswith('.') and os.path.isdir( + os.path.join(dir_of_packages, name))): + packagedirs.append(os.path.join(dir_of_packages, name)) for packagedir in packagedirs: for root, _dirs, files in os.walk(packagedir):