latest cloudtool and entity work

This commit is contained in:
Eric Froemling 2019-12-15 17:38:08 -08:00
parent 35097d28e6
commit 6adce18a4b
10 changed files with 163 additions and 51 deletions

View File

@ -1605,6 +1605,7 @@
<w>testcapi</w>
<w>testcapimodule</w>
<w>testclass</w>
<w>testd</w>
<w>testfoo</w>
<w>testfooooo</w>
<w>testhelpers</w>

View File

@ -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
################################################################################

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,6 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<!--DOCSHASH=b06760caff5d35273e974c2601857348-->
<h4><em>last updated on 2019-12-13 for Ballistica version 1.5.0 build 20001</em></h4>
<!--DOCSHASH=22f3aab06ee39a80fe0b0db52e6bef03-->
<h4><em>last updated on 2019-12-14 for Ballistica version 1.5.0 build 20001</em></h4>
<p>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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>

View File

@ -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}'

View File

@ -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

View File

@ -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

View File

@ -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):