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

View File

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

View File

@ -1,6 +1,6 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<!--DOCSHASH=096fe9ca51969f120799a3a00bd55cfb-->
<h4><em>last updated on 2019-12-19 for Ballistica version 1.5.0 build 20001</em></h4>
<!--DOCSHASH=2f4ba682ad3b48baa2f8a93eb9948cb0-->
<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,
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

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

View File

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