latest entity work

This commit is contained in:
Eric Froemling 2019-12-21 13:39:44 -08:00
parent 75e289e234
commit 355d975803
5 changed files with 150 additions and 20 deletions

View File

@ -219,6 +219,7 @@
<w>ccompiler</w> <w>ccompiler</w>
<w>cdrk</w> <w>cdrk</w>
<w>cdull</w> <w>cdull</w>
<w>cdval</w>
<w>centeuro</w> <w>centeuro</w>
<w>centiseconds</w> <w>centiseconds</w>
<w>cfconfig</w> <w>cfconfig</w>
@ -287,6 +288,7 @@
<w>compat</w> <w>compat</w>
<w>compileall</w> <w>compileall</w>
<w>compilelocations</w> <w>compilelocations</w>
<w>compounddict</w>
<w>compoundlist</w> <w>compoundlist</w>
<w>configerror</w> <w>configerror</w>
<w>confighash</w> <w>confighash</w>
@ -518,6 +520,7 @@
<w>farthestpt</w> <w>farthestpt</w>
<w>fbase</w> <w>fbase</w>
<w>fclose</w> <w>fclose</w>
<w>fcmd</w>
<w>fcntlmodule</w> <w>fcntlmodule</w>
<w>fcode</w> <w>fcode</w>
<w>fcontents</w> <w>fcontents</w>
@ -591,6 +594,7 @@
<w>fpathrel</w> <w>fpathrel</w>
<w>fpathshort</w> <w>fpathshort</w>
<w>fprint</w> <w>fprint</w>
<w>fproject</w>
<w>fpsc</w> <w>fpsc</w>
<w>framerate</w> <w>framerate</w>
<w>freefly</w> <w>freefly</w>

View File

@ -30,6 +30,7 @@ from bafoundation.entity._support import (BaseField, BoundCompoundValue,
BoundListField, BoundDictField, BoundListField, BoundDictField,
BoundCompoundListField, BoundCompoundListField,
BoundCompoundDictField) BoundCompoundDictField)
from bafoundation.entity.util import have_matching_fields
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Dict, Type, List, Any 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 a FieldInspector we return a sub-field FieldInspector.
# When accessed on an instance we return a BoundListField. # When accessed on an instance we return a BoundListField.
# noinspection DuplicatedCode
if TYPE_CHECKING: if TYPE_CHECKING:
# Access via type gives our field; via an instance gives a bound field. # 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) # change the dict, but we can prune completely if empty (and allowed)
return not data and not self._store_default return not data and not self._store_default
# noinspection DuplicatedCode
if TYPE_CHECKING: if TYPE_CHECKING:
# Return our field if accessed via type and bound-dict-field # 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. # We can also optionally prune the whole list if empty and allowed.
return not data and not self._store_default return not data and not self._store_default
# noinspection DuplicatedCode
if TYPE_CHECKING: if TYPE_CHECKING:
@overload @overload
@ -370,10 +374,10 @@ class CompoundListField(BaseField, Generic[TC]):
... ...
# Note: # 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 # a raw list of CompoundValue objects, but at runtime we actually
# always deal with BoundCompoundValue objects (see note in # always deal with BoundCompoundValue objects (see note in
# BoundCompoundListField) # BoundCompoundListField for why we accept CompoundValue objs)
@overload @overload
def __set__(self, obj: Any, value: List[TC]) -> None: 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: def set_with_data(self, data: Any, value: Any, error: bool) -> Any:
# If we were passed a BoundCompoundListField, # 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. # is what we work with natively here.
if isinstance(value, BoundCompoundListField): if isinstance(value, BoundCompoundListField):
value = list(value) value = list(value)
@ -406,10 +410,28 @@ class CompoundListField(BaseField, Generic[TC]):
# be sure the underlying data will line up; for example two # be sure the underlying data will line up; for example two
# CompoundListFields with different child_field values should not # CompoundListFields with different child_field values should not
# be inter-assignable. # be inter-assignable.
if (not all(isinstance(i, BoundCompoundValue) for i in value) if not all(isinstance(i, BoundCompoundValue) for i in value):
or not all(i.d_value is self.d_value for i in value)):
raise ValueError('CompoundListField assignment must be a ' 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], data[self.d_key] = self.filter_input([i.d_data for i in value],
error=error) error=error)
@ -468,6 +490,7 @@ class CompoundDictField(BaseField, Generic[TK, TC]):
# ONLY overriding these in type-checker land to clarify types. # ONLY overriding these in type-checker land to clarify types.
# (see note in BaseField) # (see note in BaseField)
# noinspection DuplicatedCode
if TYPE_CHECKING: if TYPE_CHECKING:
@overload @overload
@ -485,14 +508,35 @@ class CompoundDictField(BaseField, Generic[TK, TC]):
def __get__(self, obj: Any, cls: Any = None) -> Any: 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: 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: def get_with_data(self, data: Any) -> Any:
assert self.d_key in data assert self.d_key in data
return BoundCompoundDictField(self, data[self.d_key]) return BoundCompoundDictField(self, data[self.d_key])
def set_with_data(self, data: Any, value: Any, error: bool) -> Any: 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): if not isinstance(value, dict):
raise TypeError('CompoundDictField expected dict value on set.') 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 # be sure the underlying data will line up; for example two
# CompoundListFields with different child_field values should not # CompoundListFields with different child_field values should not
# be inter-assignable. # be inter-assignable.
if (not all(isinstance(i, self.d_keytype) for i in value.keys()) if (not all(isinstance(i, BoundCompoundValue)
or not all( for i in value.values())):
isinstance(i, BoundCompoundValue) for i in value.values())
or not all(i.d_value is self.d_value for i in value.values())):
raise ValueError('CompoundDictField assignment must be a ' raise ValueError('CompoundDictField assignment must be a '
'dict containing only its existing children.') 'dict containing only BoundCompoundValues.')
data[self.d_key] = {key: val.d_data for key, val in value.items()}
# 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)

View File

@ -1,6 +1,6 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<!--DOCSHASH=096fe9ca51969f120799a3a00bd55cfb--> <!--DOCSHASH=2f4ba682ad3b48baa2f8a93eb9948cb0-->
<h4><em>last updated on 2019-12-19 for Ballistica version 1.5.0 build 20001</em></h4> <h4><em>last updated on 2019-12-21 for Ballistica version 1.5.0 build 20001</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <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> 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> <hr>

View File

@ -72,7 +72,11 @@ class EntityTest(entity.Entity):
tval2 = entity.Field('t2', entity.DateTimeValue()) tval2 = entity.Field('t2', entity.DateTimeValue())
str_int_dict = entity.DictField('sd', str, entity.IntValue()) str_int_dict = entity.DictField('sd', str, entity.IntValue())
compoundlist = entity.CompoundListField('l', CompoundTest()) 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()) fval2 = entity.Field('f2', entity.Float3Value())
@ -111,8 +115,10 @@ def test_entity_values() -> None:
# Simple value dict field. # Simple value dict field.
assert 'foo' not in ent.str_int_dict assert 'foo' not in ent.str_int_dict
# Set with incorrect key type should give TypeError.
with pytest.raises(TypeError): with pytest.raises(TypeError):
ent.str_int_dict[0] = 123 # type: ignore ent.str_int_dict[0] = 123 # type: ignore
# And set with incorrect value type should do same.
with pytest.raises(TypeError): with pytest.raises(TypeError):
ent.str_int_dict['foo'] = 'bar' # type: ignore ent.str_int_dict['foo'] = 'bar' # type: ignore
ent.str_int_dict['foo'] = 123 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) # (which seems to get a bit ugly, but may be worth revisiting)
# assert dict(ent.str_int_dict) == {'foo': 123} # 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 # noinspection PyTypeHints
def test_entity_values_2() -> None: def test_entity_values_2() -> None:
@ -154,6 +163,17 @@ def test_entity_values_2() -> None:
assert len(ent.compoundlist) == 1 assert len(ent.compoundlist) == 1
assert static_type_equals(ent.compoundlist[0], CompoundTest) 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 # Enum value
with pytest.raises(ValueError): with pytest.raises(ValueError):
ent.enumval = None # type: ignore 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) assert static_type_equals(ent.grp.compoundlist[0].subval, bool)
# noinspection PyTypeHints
def test_field_copies() -> None: def test_field_copies() -> None:
"""Test copying various values between fields.""" """Test copying various values between fields."""
ent1 = EntityTest() ent1 = EntityTest()
@ -208,10 +229,43 @@ def test_field_copies() -> None:
val.isubval = 356 val.isubval = 356
assert ent1.compoundlist[0].isubval == 356 assert ent1.compoundlist[0].isubval == 356
assert len(ent1.compoundlist) == 1 assert len(ent1.compoundlist) == 1
ent1.compoundlist.append()
assert len(ent1.compoundlist) == 2
assert len(ent2.compoundlist) == 0 assert len(ent2.compoundlist) == 0
# Copying to the same field on different obj should work.
ent2.compoundlist = ent1.compoundlist ent2.compoundlist = ent1.compoundlist
assert ent2.compoundlist[0].isubval == 356 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: def test_field_access_from_type() -> None:

View File

@ -357,19 +357,26 @@ def sync_all() -> None:
This assumes that there is a 'sync-full' and 'sync-list' Makefile target This assumes that there is a 'sync-full' and 'sync-list' Makefile target
under each project. under each project.
""" """
import concurrent.futures
print(f'{CLRBLU}Updating formatting for all projects...{CLREND}') print(f'{CLRBLU}Updating formatting for all projects...{CLREND}')
projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS') projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS')
if projects_str is None: if projects_str is None:
raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.') 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 # 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 # 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 # a preflight we'll often wind up getting out-of-sync errors due to
# formatting changing after the sync. # formatting changing after the sync.
for project in projects_str.split(':'): with concurrent.futures.ThreadPoolExecutor(
cmd = f'cd "{project}" && make format' max_workers=len(projects)) as executor:
print(cmd) # Converting this to a list will propagate any errors.
subprocess.run(cmd, shell=True, check=True) list(executor.map(_format_project, projects))
if len(sys.argv) > 2 and sys.argv[2] == 'list': if len(sys.argv) > 2 and sys.argv[2] == 'list':
# List mode # List mode