mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 15:33:26 +08:00
latest cloudtool and entity work
This commit is contained in:
parent
35097d28e6
commit
6adce18a4b
1
.idea/dictionaries/ericf.xml
generated
1
.idea/dictionaries/ericf.xml
generated
@ -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>
|
||||
|
||||
2
Makefile
2
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
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user