mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
Improved efro.dataclasses functionality
This commit is contained in:
parent
efe7050445
commit
4aa178e337
@ -3936,14 +3936,14 @@
|
||||
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/82/81/1ae81275ebe9ab9ab502b7eebb32",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/10/cf/d799dd2e5e833a42330a91a27872",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/66/4b663b0bd3d346168132a3b76e3d",
|
||||
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/26/e42cb423191515afb14ed85da586",
|
||||
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/90/5d/b4f96e9882d98e83a7ba33c9db22",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/14/f7/5ad112c496608b9cb82bf33f8bb7",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/46/ca/13bc7ce2e07eeee54c3309c960b7",
|
||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/79/65/c7a4dcab9c1f5f0b17ded9d2a0ac",
|
||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/c1/151d5e57bf09762496470bf8c4bb",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0c/e1/2335a8137464182089e57ace0f8e",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/30/a7/af58cca4a647d19fca0a43b11d88",
|
||||
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/21/e9/1430bf257d1263ff0ed36410c332",
|
||||
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/46/7a/caed7137d70eb5bf1696a879fb75",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d5/e5/c6ebd43787923294b2210592290d",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/23/2b/74da96e45034770834c5213cfaf0",
|
||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/11/f6/ff5475943c5c106266121392faab",
|
||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/71/d5/009c34dd2016fa879c28c8838405",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/85/c8/19cf729b848e002fdc731decba21",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bc/7b/c93b714145994b823a708b57b501",
|
||||
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e8/bb/5985b1670513fbcd4931383af8a0",
|
||||
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/69/51/2fd61101ec41e2944db05fa588db",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a6/9b/f3c55efe1ae7a8fd3ba35d3f9caa",
|
||||
@ -3952,12 +3952,12 @@
|
||||
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/83/25/980050d75bbea49a84652209050c",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/aa/e82233695a50974e7e22db4e7146",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2b/45/7f9fbae208890455fce2fbc172d3",
|
||||
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/4c/5d82b9c31124da7a329358fcd553",
|
||||
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ad/71/ed5d6a378aca34d43483aed9dc79",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/30/e5/284e85af992a18c1053adc58cdfb",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/23/ca/f4cfaabc60d0afa57b6c0f38547f",
|
||||
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0e/47/cf7099fc7b7317c2be02861e6978",
|
||||
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/ef/7f5408bb25f0ebcf17e52ad18442",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/04/42c2bf1a43ec9c2c674289f5b963",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/5d/06f1fc4de7443709de1acfc00960"
|
||||
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/f2/67f1041d8c49e7bdad100dae4ef1",
|
||||
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/80/7b/f57451f57d2434bcb75d48c08a33",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c6/ce/1df69a933f207867f8385f491f47",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/21/49/0b011f4ce5872c7eaabaa97b890f",
|
||||
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/6f/2fc08ac6c2b2c3b31572a369deab",
|
||||
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1d/bf/ce1ea8ad2fb7bcf58a367ec1e19a",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fb/0b/864980222e52ee6bd5a272b0f742",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bb/6d/401e3cb8d8641df64ffbeac2b02d"
|
||||
}
|
||||
21
.idea/dictionaries/ericf.xml
generated
21
.idea/dictionaries/ericf.xml
generated
@ -174,12 +174,14 @@
|
||||
<w>basew</w>
|
||||
<w>bastd</w>
|
||||
<w>batools</w>
|
||||
<w>bbbb</w>
|
||||
<w>bblk</w>
|
||||
<w>bblu</w>
|
||||
<w>bbot</w>
|
||||
<w>bbtn</w>
|
||||
<w>bcppcompiler</w>
|
||||
<w>bcyn</w>
|
||||
<w>bdea</w>
|
||||
<w>bdfl</w>
|
||||
<w>bdir</w>
|
||||
<w>belarussian</w>
|
||||
@ -314,6 +316,7 @@
|
||||
<w>cheadersline</w>
|
||||
<w>checkarg</w>
|
||||
<w>checkboxwidget</w>
|
||||
<w>checkchisel</w>
|
||||
<w>checkenv</w>
|
||||
<w>checkfast</w>
|
||||
<w>checkfull</w>
|
||||
@ -592,6 +595,7 @@
|
||||
<w>efrocachemap</w>
|
||||
<w>efroemling</w>
|
||||
<w>efrogradle</w>
|
||||
<w>efrohack</w>
|
||||
<w>efrohome</w>
|
||||
<w>efrosync</w>
|
||||
<w>efrotool</w>
|
||||
@ -623,6 +627,8 @@
|
||||
<w>entrytypeselect</w>
|
||||
<w>enumtype</w>
|
||||
<w>enumval</w>
|
||||
<w>enumvalue</w>
|
||||
<w>enval</w>
|
||||
<w>envcfg</w>
|
||||
<w>envhash</w>
|
||||
<w>envname</w>
|
||||
@ -697,6 +703,8 @@
|
||||
<w>fhashes</w>
|
||||
<w>fhdr</w>
|
||||
<w>fieldattr</w>
|
||||
<w>fieldname</w>
|
||||
<w>fieldpath</w>
|
||||
<w>fieldsdict</w>
|
||||
<w>fieldtype</w>
|
||||
<w>fieldtypes</w>
|
||||
@ -1021,6 +1029,7 @@
|
||||
<w>inputfiles</w>
|
||||
<w>inputhash</w>
|
||||
<w>inputnode</w>
|
||||
<w>inputter</w>
|
||||
<w>inputtype</w>
|
||||
<w>inpututils</w>
|
||||
<w>inspectdir</w>
|
||||
@ -1117,6 +1126,7 @@
|
||||
<w>leaderboards</w>
|
||||
<w>leady</w>
|
||||
<w>lenglishvalues</w>
|
||||
<w>lenval</w>
|
||||
<w>levelgametype</w>
|
||||
<w>levelmodule</w>
|
||||
<w>levelname</w>
|
||||
@ -1413,6 +1423,7 @@
|
||||
<w>numedit</w>
|
||||
<w>numsound</w>
|
||||
<w>numstr</w>
|
||||
<w>nval</w>
|
||||
<w>nvcompress</w>
|
||||
<w>nvidia</w>
|
||||
<w>nyko</w>
|
||||
@ -1424,6 +1435,7 @@
|
||||
<w>obval</w>
|
||||
<w>occurrances</w>
|
||||
<w>oculus</w>
|
||||
<w>oenval</w>
|
||||
<w>offsanchor</w>
|
||||
<w>ofval</w>
|
||||
<w>oggenc</w>
|
||||
@ -1465,6 +1477,8 @@
|
||||
<w>outhashpath</w>
|
||||
<w>outname</w>
|
||||
<w>outpath</w>
|
||||
<w>outputter</w>
|
||||
<w>outvalue</w>
|
||||
<w>ouya</w>
|
||||
<w>overloadsigs</w>
|
||||
<w>packagedir</w>
|
||||
@ -1829,6 +1843,8 @@
|
||||
<w>sdtk</w>
|
||||
<w>selectmodule</w>
|
||||
<w>senze</w>
|
||||
<w>seqtype</w>
|
||||
<w>seqtypestr</w>
|
||||
<w>serverbuild</w>
|
||||
<w>servercallthread</w>
|
||||
<w>servercallthreadtype</w>
|
||||
@ -1882,6 +1898,7 @@
|
||||
<w>shroom</w>
|
||||
<w>shutil</w>
|
||||
<w>simplesubclasses</w>
|
||||
<w>simpletype</w>
|
||||
<w>sincelaunch</w>
|
||||
<w>singledispatch</w>
|
||||
<w>singledispatchmethod</w>
|
||||
@ -1951,6 +1968,7 @@
|
||||
<w>sred</w>
|
||||
<w>sshd</w>
|
||||
<w>sslproto</w>
|
||||
<w>ssval</w>
|
||||
<w>stackstr</w>
|
||||
<w>standin</w>
|
||||
<w>starscale</w>
|
||||
@ -1995,6 +2013,7 @@
|
||||
<w>subdep</w>
|
||||
<w>subdeps</w>
|
||||
<w>subdirs</w>
|
||||
<w>subfieldpath</w>
|
||||
<w>subfolders</w>
|
||||
<w>subname</w>
|
||||
<w>subpath</w>
|
||||
@ -2005,6 +2024,7 @@
|
||||
<w>subprocesses</w>
|
||||
<w>subrepos</w>
|
||||
<w>subsel</w>
|
||||
<w>subtypestr</w>
|
||||
<w>subval</w>
|
||||
<w>subvalue</w>
|
||||
<w>subvaluetype</w>
|
||||
@ -2074,6 +2094,7 @@
|
||||
<w>tempfile</w>
|
||||
<w>tempfilepath</w>
|
||||
<w>templatecb</w>
|
||||
<w>tenum</w>
|
||||
<w>termcolors</w>
|
||||
<w>termios</w>
|
||||
<w>testbuffer</w>
|
||||
|
||||
6
Makefile
6
Makefile
@ -654,17 +654,17 @@ test-full: test
|
||||
|
||||
# Individual test with extra output enabled.
|
||||
test-assetmanager:
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
|
||||
tests/test_ba/test_assetmanager.py::test_assetmanager
|
||||
|
||||
# Individual test with extra output enabled.
|
||||
test-dataclasses:
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
|
||||
tests/test_efro/test_dataclasses.py
|
||||
|
||||
# Individual test with extra output enabled.
|
||||
test-entity:
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
|
||||
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
|
||||
tests/test_efro/test_entity.py
|
||||
|
||||
# Tell make which of these targets don't represent files.
|
||||
|
||||
@ -60,8 +60,7 @@ from ba._campaign import Campaign
|
||||
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
|
||||
timestring, cameraflash)
|
||||
from ba._general import (WeakCall, Call, existing, Existable,
|
||||
verify_object_death, storagename, getclass,
|
||||
enum_by_value)
|
||||
verify_object_death, storagename, getclass)
|
||||
from ba._keyboard import Keyboard
|
||||
from ba._level import Level
|
||||
from ba._lobby import Lobby, Chooser
|
||||
|
||||
@ -8,7 +8,6 @@ import types
|
||||
import weakref
|
||||
import random
|
||||
import inspect
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, TypeVar, Protocol
|
||||
|
||||
from efro.terminal import Clr
|
||||
@ -36,7 +35,6 @@ class Existable(Protocol):
|
||||
|
||||
ExistableType = TypeVar('ExistableType', bound=Existable)
|
||||
T = TypeVar('T')
|
||||
ET = TypeVar('ET', bound=Enum)
|
||||
|
||||
|
||||
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
|
||||
@ -399,30 +397,3 @@ def storagename(suffix: str = None) -> str:
|
||||
if suffix is not None:
|
||||
fullpath = f'{fullpath}_{suffix}'
|
||||
return fullpath.replace('.', '_')
|
||||
|
||||
|
||||
def enum_by_value(cls: Type[ET], value: Any) -> ET:
|
||||
"""Create an enum from a value.
|
||||
|
||||
Category: General Utility Functions
|
||||
|
||||
This is basically the same as doing 'obj = EnumType(value)' except
|
||||
that it works around an issue where a reference loop is created
|
||||
if an exception is thrown due to an invalid value. Since we disable
|
||||
the cyclic garbage collector for most of the time, such loops can lead
|
||||
to our objects sticking around longer than we want.
|
||||
This issue has been submitted to Python as a bug so hopefully we can
|
||||
remove this eventually if it gets fixed: https://bugs.python.org/issue42248
|
||||
"""
|
||||
|
||||
# Note: we don't recreate *ALL* the functionality of the Enum constructor
|
||||
# such as the _missing_ hook; but this should cover our basic needs.
|
||||
value2member_map = getattr(cls, '_value2member_map_')
|
||||
assert value2member_map is not None
|
||||
try:
|
||||
out = value2member_map[value]
|
||||
assert isinstance(out, cls)
|
||||
return out
|
||||
except KeyError:
|
||||
raise ValueError('%r is not a valid %s' %
|
||||
(value, cls.__name__)) from None
|
||||
|
||||
@ -241,6 +241,7 @@ class GatherWindow(ba.Window):
|
||||
ba.print_exception(f'Error saving state for {self}.')
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
from efro.util import enum_by_value
|
||||
try:
|
||||
for tab in self._tabs.values():
|
||||
tab.restore_state()
|
||||
@ -252,7 +253,7 @@ class GatherWindow(ba.Window):
|
||||
current_tab = self.TabID.ABOUT
|
||||
gather_tab_val = ba.app.config.get('Gather Tab')
|
||||
try:
|
||||
stored_tab = ba.enum_by_value(self.TabID, gather_tab_val)
|
||||
stored_tab = enum_by_value(self.TabID, gather_tab_val)
|
||||
if stored_tab in self._tab_row.tabs:
|
||||
current_tab = stored_tab
|
||||
except ValueError:
|
||||
@ -264,8 +265,8 @@ class GatherWindow(ba.Window):
|
||||
sel = self._tab_container
|
||||
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
|
||||
try:
|
||||
sel_tab_id = ba.enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
sel_tab_id = enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
except ValueError:
|
||||
sel_tab_id = self.TabID.ABOUT
|
||||
sel = self._tab_row.tabs[sel_tab_id].button
|
||||
|
||||
@ -1018,6 +1018,7 @@ class StoreBrowserWindow(ba.Window):
|
||||
ba.print_exception(f'Error saving state for {self}.')
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
from efro.util import enum_by_value
|
||||
try:
|
||||
sel: Optional[ba.Widget]
|
||||
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
|
||||
@ -1025,8 +1026,8 @@ class StoreBrowserWindow(ba.Window):
|
||||
assert isinstance(sel_name, (str, type(None)))
|
||||
|
||||
try:
|
||||
current_tab = ba.enum_by_value(self.TabID,
|
||||
ba.app.config.get('Store Tab'))
|
||||
current_tab = enum_by_value(self.TabID,
|
||||
ba.app.config.get('Store Tab'))
|
||||
except ValueError:
|
||||
current_tab = self.TabID.CHARACTERS
|
||||
|
||||
@ -1040,8 +1041,8 @@ class StoreBrowserWindow(ba.Window):
|
||||
sel = self._scrollwidget
|
||||
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
|
||||
try:
|
||||
sel_tab_id = ba.enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
sel_tab_id = enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
except ValueError:
|
||||
sel_tab_id = self.TabID.CHARACTERS
|
||||
sel = self._tab_row.tabs[sel_tab_id].button
|
||||
|
||||
@ -503,14 +503,15 @@ class WatchWindow(ba.Window):
|
||||
ba.print_exception(f'Error saving state for {self}.')
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
from efro.util import enum_by_value
|
||||
try:
|
||||
sel: Optional[ba.Widget]
|
||||
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
|
||||
{}).get('sel_name')
|
||||
assert isinstance(sel_name, (str, type(None)))
|
||||
try:
|
||||
current_tab = ba.enum_by_value(self.TabID,
|
||||
ba.app.config.get('Watch Tab'))
|
||||
current_tab = enum_by_value(self.TabID,
|
||||
ba.app.config.get('Watch Tab'))
|
||||
except ValueError:
|
||||
current_tab = self.TabID.MY_REPLAYS
|
||||
self._set_tab(current_tab)
|
||||
@ -521,8 +522,8 @@ class WatchWindow(ba.Window):
|
||||
sel = self._tab_container
|
||||
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
|
||||
try:
|
||||
sel_tab_id = ba.enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
sel_tab_id = enum_by_value(self.TabID,
|
||||
sel_name.split(':')[-1])
|
||||
except ValueError:
|
||||
sel_tab_id = self.TabID.MY_REPLAYS
|
||||
sel = self._tab_row.tabs[sel_tab_id].button
|
||||
|
||||
@ -23,7 +23,7 @@ sys.path += [
|
||||
]
|
||||
|
||||
from bacommon.servermanager import ServerConfig, StartServerModeCommand
|
||||
from efro.dataclasses import dataclass_assign, dataclass_validate
|
||||
from efro.dataclasses import dataclass_from_dict, dataclass_validate
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
|
||||
@ -32,9 +32,11 @@ if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
from bacommon.servermanager import ServerCommand
|
||||
|
||||
VERSION_STR = '1.1.0'
|
||||
VERSION_STR = '1.1.1'
|
||||
|
||||
# Version history:
|
||||
# 1.1.1:
|
||||
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
|
||||
# 1.1.0:
|
||||
# Added shutdown command
|
||||
# Changed restart to default to immediate=True
|
||||
@ -274,19 +276,17 @@ class ServerManagerApp:
|
||||
def _load_config(self) -> ServerConfig:
|
||||
user_config_path = 'config.yaml'
|
||||
|
||||
# Start with a default config, and if there is a config.yaml,
|
||||
# assign whatever is contained within.
|
||||
config = ServerConfig()
|
||||
if os.path.exists(user_config_path):
|
||||
import yaml
|
||||
with open(user_config_path) as infile:
|
||||
user_config = yaml.safe_load(infile.read())
|
||||
user_config_raw = yaml.safe_load(infile.read())
|
||||
|
||||
# An empty config file will yield None, and that's ok.
|
||||
if user_config is not None:
|
||||
dataclass_assign(config, user_config)
|
||||
if user_config_raw is not None:
|
||||
return dataclass_from_dict(ServerConfig, user_config_raw)
|
||||
|
||||
return config
|
||||
# Go with defaults if we weren't able to load anything.
|
||||
return ServerConfig()
|
||||
|
||||
def _enable_tab_completion(self, locs: Dict) -> None:
|
||||
"""Enable tab-completion on platforms where available (linux/mac)."""
|
||||
|
||||
20
ballisticacore-cmake/.idea/dictionaries/ericf.xml
generated
20
ballisticacore-cmake/.idea/dictionaries/ericf.xml
generated
@ -70,6 +70,7 @@
|
||||
<w>bbbbbb</w>
|
||||
<w>bbbbbbb</w>
|
||||
<w>bcfn</w>
|
||||
<w>bdea</w>
|
||||
<w>bezanson</w>
|
||||
<w>bgra</w>
|
||||
<w>bigendian</w>
|
||||
@ -142,6 +143,7 @@
|
||||
<w>charstr</w>
|
||||
<w>chatmessage</w>
|
||||
<w>checkboxwidget</w>
|
||||
<w>checkchisel</w>
|
||||
<w>chrono</w>
|
||||
<w>chunksize</w>
|
||||
<w>cjief</w>
|
||||
@ -246,6 +248,7 @@
|
||||
<w>echidna</w>
|
||||
<w>edef</w>
|
||||
<w>efro</w>
|
||||
<w>efrohack</w>
|
||||
<w>efrohome</w>
|
||||
<w>elems</w>
|
||||
<w>elevenbase</w>
|
||||
@ -258,6 +261,8 @@
|
||||
<w>endline</w>
|
||||
<w>endtime</w>
|
||||
<w>entrypoint</w>
|
||||
<w>enumvalue</w>
|
||||
<w>enval</w>
|
||||
<w>envcfg</w>
|
||||
<w>envs</w>
|
||||
<w>envval</w>
|
||||
@ -297,6 +302,8 @@
|
||||
<w>fffffff</w>
|
||||
<w>fffffffffifff</w>
|
||||
<w>fgets</w>
|
||||
<w>fieldname</w>
|
||||
<w>fieldpath</w>
|
||||
<w>fifteenbits</w>
|
||||
<w>filterval</w>
|
||||
<w>finishedptr</w>
|
||||
@ -427,6 +434,7 @@
|
||||
<w>initguid</w>
|
||||
<w>inittab</w>
|
||||
<w>inputdevice</w>
|
||||
<w>inputter</w>
|
||||
<w>insta</w>
|
||||
<w>intercollide</w>
|
||||
<w>internalformat</w>
|
||||
@ -473,6 +481,7 @@
|
||||
<w>lastvalid</w>
|
||||
<w>leaderboard</w>
|
||||
<w>leaderboards</w>
|
||||
<w>lenval</w>
|
||||
<w>lgui</w>
|
||||
<w>lhalf</w>
|
||||
<w>libutf</w>
|
||||
@ -593,6 +602,7 @@
|
||||
<w>numc</w>
|
||||
<w>numentries</w>
|
||||
<w>numlock</w>
|
||||
<w>nval</w>
|
||||
<w>nvidia</w>
|
||||
<w>nyffenegger</w>
|
||||
<w>objexists</w>
|
||||
@ -600,6 +610,7 @@
|
||||
<w>obstack</w>
|
||||
<w>obvs</w>
|
||||
<w>oculus</w>
|
||||
<w>oenval</w>
|
||||
<w>oiffsss</w>
|
||||
<w>oldname</w>
|
||||
<w>oooo</w>
|
||||
@ -630,7 +641,9 @@
|
||||
<w>ostype</w>
|
||||
<w>ourself</w>
|
||||
<w>ourstanding</w>
|
||||
<w>outputter</w>
|
||||
<w>outval</w>
|
||||
<w>outvalue</w>
|
||||
<w>ouya</w>
|
||||
<w>parameteriv</w>
|
||||
<w>passcode</w>
|
||||
@ -752,6 +765,8 @@
|
||||
<w>sdl's</w>
|
||||
<w>sdlk</w>
|
||||
<w>seqlen</w>
|
||||
<w>seqtype</w>
|
||||
<w>seqtypestr</w>
|
||||
<w>serv</w>
|
||||
<w>serverget</w>
|
||||
<w>serverput</w>
|
||||
@ -780,6 +795,7 @@
|
||||
<w>shufflable</w>
|
||||
<w>signsubscale</w>
|
||||
<w>simd</w>
|
||||
<w>simpletype</w>
|
||||
<w>sisssssssss</w>
|
||||
<w>sixteenbits</w>
|
||||
<w>smoothering</w>
|
||||
@ -810,6 +826,7 @@
|
||||
<w>sssssssd</w>
|
||||
<w>sssssssi</w>
|
||||
<w>ssssssssssss</w>
|
||||
<w>ssval</w>
|
||||
<w>standin</w>
|
||||
<w>startedptr</w>
|
||||
<w>startpos</w>
|
||||
@ -832,11 +849,13 @@
|
||||
<w>subargs</w>
|
||||
<w>subclsssing</w>
|
||||
<w>subentities</w>
|
||||
<w>subfieldpath</w>
|
||||
<w>subitems</w>
|
||||
<w>subpaths</w>
|
||||
<w>subplatform</w>
|
||||
<w>subscale</w>
|
||||
<w>subscr</w>
|
||||
<w>subtypestr</w>
|
||||
<w>sval</w>
|
||||
<w>symbolification</w>
|
||||
<w>syscalls</w>
|
||||
@ -849,6 +868,7 @@
|
||||
<w>teleported</w>
|
||||
<w>teleporting</w>
|
||||
<w>tempvec</w>
|
||||
<w>tenum</w>
|
||||
<w>testint</w>
|
||||
<w>testnode</w>
|
||||
<w>texel</w>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2021-01-08 for Ballistica version 1.5.30 build 20266</em></h4>
|
||||
<h4><em>last updated on 2021-01-14 for Ballistica version 1.5.30 build 20267</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>
|
||||
@ -86,7 +86,6 @@
|
||||
<ul>
|
||||
<li><a href="#function_ba_charstr">ba.charstr()</a></li>
|
||||
<li><a href="#function_ba_do_once">ba.do_once()</a></li>
|
||||
<li><a href="#function_ba_enum_by_value">ba.enum_by_value()</a></li>
|
||||
<li><a href="#function_ba_garbage_collect">ba.garbage_collect()</a></li>
|
||||
<li><a href="#function_ba_getclass">ba.getclass()</a></li>
|
||||
<li><a href="#function_ba_is_browser_likely_available">ba.is_browser_likely_available()</a></li>
|
||||
@ -6579,22 +6578,6 @@ the background and just looks pretty; it does not affect gameplay.
|
||||
Note that the actual amount emitted may vary depending on graphics
|
||||
settings, exiting element counts, or other factors.</p>
|
||||
|
||||
<hr>
|
||||
<h2><strong><a name="function_ba_enum_by_value">ba.enum_by_value()</a></strong></h3>
|
||||
<p><span>enum_by_value(cls: Type[ET], value: Any) -> ET</span></p>
|
||||
|
||||
<p>Create an enum from a value.</p>
|
||||
|
||||
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
|
||||
|
||||
<p>This is basically the same as doing 'obj = EnumType(value)' except
|
||||
that it works around an issue where a reference loop is created
|
||||
if an exception is thrown due to an invalid value. Since we disable
|
||||
the cyclic garbage collector for most of the time, such loops can lead
|
||||
to our objects sticking around longer than we want.
|
||||
This issue has been submitted to Python as a bug so hopefully we can
|
||||
remove this eventually if it gets fixed: https://bugs.python.org/issue42248</p>
|
||||
|
||||
<hr>
|
||||
<h2><strong><a name="function_ba_existing">ba.existing()</a></strong></h3>
|
||||
<p><span>existing(obj: Optional[ExistableType]) -> Optional[ExistableType]</span></p>
|
||||
|
||||
@ -4,19 +4,33 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from efro.dataclasses import dataclass_assign, dataclass_validate
|
||||
from efro.dataclasses import (dataclass_validate, dataclass_from_dict,
|
||||
dataclass_to_dict)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Set
|
||||
|
||||
|
||||
class _EnumTest(Enum):
|
||||
TEST1 = 'test1'
|
||||
TEST2 = 'test2'
|
||||
|
||||
|
||||
@dataclass
|
||||
class _NestedClass:
|
||||
ival: int = 0
|
||||
sval: str = 'foo'
|
||||
|
||||
|
||||
def test_assign() -> None:
|
||||
"""Testing various assignments."""
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
@dataclass
|
||||
@ -25,120 +39,181 @@ def test_assign() -> None:
|
||||
sval: str = ''
|
||||
bval: bool = True
|
||||
fval: float = 1.0
|
||||
nval: _NestedClass = field(default_factory=_NestedClass)
|
||||
enval: _EnumTest = _EnumTest.TEST1
|
||||
oival: Optional[int] = None
|
||||
osval: Optional[str] = None
|
||||
obval: Optional[bool] = None
|
||||
ofval: Optional[float] = None
|
||||
oenval: Optional[_EnumTest] = _EnumTest.TEST1
|
||||
lsval: List[str] = field(default_factory=list)
|
||||
lival: List[int] = field(default_factory=list)
|
||||
lbval: List[bool] = field(default_factory=list)
|
||||
lfval: List[float] = field(default_factory=list)
|
||||
|
||||
tclass = _TestClass()
|
||||
lenval: List[_EnumTest] = field(default_factory=list)
|
||||
ssval: Set[str] = field(default_factory=set)
|
||||
|
||||
class _TestClass2:
|
||||
pass
|
||||
|
||||
tclass2 = _TestClass2()
|
||||
|
||||
# Arg types:
|
||||
# Attempting to use with non-dataclass should fail.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass2, {})
|
||||
dataclass_from_dict(_TestClass2, {})
|
||||
|
||||
# Attempting to pass non-dicts should fail.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, []) # type: ignore
|
||||
dataclass_from_dict(_TestClass, []) # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, None) # type: ignore
|
||||
|
||||
# Invalid attrs.
|
||||
# Passing an attr not in the dataclass should fail.
|
||||
with pytest.raises(AttributeError):
|
||||
dataclass_assign(tclass, {'nonexistent': 'foo'})
|
||||
dataclass_from_dict(_TestClass, {'nonexistent': 'foo'})
|
||||
|
||||
# Correct types.
|
||||
dataclass_assign(
|
||||
tclass, {
|
||||
# A dict containing *ALL* values should match what we
|
||||
# get when creating a dataclass and then converting back
|
||||
# to a dict.
|
||||
dict1 = {
|
||||
'ival': 1,
|
||||
'sval': 'foo',
|
||||
'bval': True,
|
||||
'fval': 2.0,
|
||||
'nval': {
|
||||
'ival': 1,
|
||||
'sval': 'foo',
|
||||
'bval': True,
|
||||
'fval': 2.0,
|
||||
'lsval': ['foo'],
|
||||
'lival': [10],
|
||||
'lbval': [False],
|
||||
'lfval': [1.0]
|
||||
})
|
||||
dataclass_assign(
|
||||
tclass, {
|
||||
'oival': None,
|
||||
'osval': None,
|
||||
'obval': None,
|
||||
'ofval': None,
|
||||
'lsval': [],
|
||||
'lival': [],
|
||||
'lbval': [],
|
||||
'lfval': []
|
||||
})
|
||||
dataclass_assign(
|
||||
tclass, {
|
||||
'oival': 1,
|
||||
'osval': 'foo',
|
||||
'obval': True,
|
||||
'ofval': 2.0,
|
||||
'lsval': ['foo', 'bar', 'eep'],
|
||||
'lival': [10, 11, 12],
|
||||
'lbval': [False, True],
|
||||
'lfval': [1.0, 2.0, 3.0]
|
||||
})
|
||||
'sval': 'bar'
|
||||
},
|
||||
'enval': 'test1',
|
||||
'oival': 1,
|
||||
'osval': 'foo',
|
||||
'obval': True,
|
||||
'ofval': 1.0,
|
||||
'oenval': 'test2',
|
||||
'lsval': ['foo'],
|
||||
'lival': [10],
|
||||
'lbval': [False],
|
||||
'lfval': [1.0],
|
||||
'lenval': ['test1', 'test2'],
|
||||
'ssval': ['foo']
|
||||
}
|
||||
dc1 = dataclass_from_dict(_TestClass, dict1)
|
||||
assert dataclass_to_dict(dc1) == dict1
|
||||
|
||||
# Type mismatches.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ival': 'foo'})
|
||||
# A few other assignment checks.
|
||||
assert isinstance(
|
||||
dataclass_from_dict(
|
||||
_TestClass, {
|
||||
'oival': None,
|
||||
'osval': None,
|
||||
'obval': None,
|
||||
'ofval': None,
|
||||
'lsval': [],
|
||||
'lival': [],
|
||||
'lbval': [],
|
||||
'lfval': [],
|
||||
'ssval': []
|
||||
}), _TestClass)
|
||||
assert isinstance(
|
||||
dataclass_from_dict(
|
||||
_TestClass, {
|
||||
'oival': 1,
|
||||
'osval': 'foo',
|
||||
'obval': True,
|
||||
'ofval': 2.0,
|
||||
'lsval': ['foo', 'bar', 'eep'],
|
||||
'lival': [10, 11, 12],
|
||||
'lbval': [False, True],
|
||||
'lfval': [1.0, 2.0, 3.0]
|
||||
}), _TestClass)
|
||||
|
||||
# Attr assigns mismatched with their value types should fail.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'sval': 1})
|
||||
dataclass_from_dict(_TestClass, {'ival': 'foo'})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'sval': 1})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'bval': 2})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'oival': 'foo'})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'osval': 1})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'obval': 2})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'ofval': 'blah'})
|
||||
with pytest.raises(ValueError):
|
||||
dataclass_from_dict(_TestClass, {'oenval': 'test3'})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lsval': 'blah'})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lsval': ['blah', None]})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lsval': [1]})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lsval': (1, )})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lbval': [None]})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lival': ['foo']})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lfval': [True]})
|
||||
with pytest.raises(ValueError):
|
||||
dataclass_from_dict(_TestClass, {'lenval': ['test1', 'test3']})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'ssval': [True]})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'ssval': {}})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'ssval': set()})
|
||||
|
||||
# More subtle attr/type mismatches that should fail
|
||||
# (we currently require EXACT type matches).
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'bval': 2})
|
||||
dataclass_from_dict(_TestClass, {'ival': True})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'fval': 2}, coerce_to_float=False)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'bval': 1})
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'ofval': 1}, coerce_to_float=False)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_from_dict(_TestClass, {'lfval': [1]}, coerce_to_float=False)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'oival': 'foo'})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'osval': 1})
|
||||
def test_coerce() -> None:
|
||||
"""Test value coercion."""
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'obval': 2})
|
||||
@dataclass
|
||||
class _TestClass:
|
||||
ival: int = 0
|
||||
fval: float = 0.0
|
||||
|
||||
# Float value present for int should never work.
|
||||
obj = _TestClass()
|
||||
# noinspection PyTypeHints
|
||||
obj.ival = 1.0 # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ofval': 'blah'})
|
||||
dataclass_validate(obj, coerce_to_float=True)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_validate(obj, coerce_to_float=False)
|
||||
|
||||
# Int value present for float should work only with coerce on.
|
||||
obj = _TestClass()
|
||||
obj.fval = 1
|
||||
dataclass_validate(obj, coerce_to_float=True)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lsval': 'blah'})
|
||||
dataclass_validate(obj, coerce_to_float=False)
|
||||
|
||||
# Likewise, passing in an int for a float field should work only
|
||||
# with coerce on.
|
||||
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=True)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lsval': [1]})
|
||||
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=False)
|
||||
|
||||
# Passing in floats for an int field should never work.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lbval': [None]})
|
||||
|
||||
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=True)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lival': ['foo']})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lfval': [True]})
|
||||
|
||||
# More subtle ones (we currently require EXACT type matches)
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ival': True})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'fval': 2})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'bval': 1})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ofval': 1})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'lfval': [1]})
|
||||
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=False)
|
||||
|
||||
|
||||
def test_validate() -> None:
|
||||
@ -159,10 +234,10 @@ def test_validate() -> None:
|
||||
tclass = _TestClass()
|
||||
dataclass_validate(tclass)
|
||||
|
||||
# No longer valid.
|
||||
# No longer valid (without coerce)
|
||||
tclass.fval = 1
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_validate(tclass)
|
||||
dataclass_validate(tclass, coerce_to_float=False)
|
||||
|
||||
# Should pass by default.
|
||||
tclass = _TestClass()
|
||||
|
||||
@ -1,122 +1,295 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Custom functionality for dealing with dataclasses."""
|
||||
# Note: We do lots of comparing of exact types here which is normally
|
||||
# frowned upon (stuff like isinstance() is usually encouraged).
|
||||
# pylint: disable=unidiomatic-typecheck
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import TYPE_CHECKING
|
||||
import inspect
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic
|
||||
|
||||
from efro.util import enum_by_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Type, Tuple
|
||||
from typing import Any, Dict, Type, Tuple, Optional
|
||||
|
||||
# For fields with these string types, we require a passed value's type
|
||||
# to exactly match one of the tuple values to consider the assignment valid.
|
||||
_SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = {
|
||||
'int': (int, ),
|
||||
'str': (str, ),
|
||||
'bool': (bool, ),
|
||||
'float': (float, ),
|
||||
'Optional[int]': (int, type(None)),
|
||||
'Optional[str]': (str, type(None)),
|
||||
'Optional[bool]': (bool, type(None)),
|
||||
'Optional[float]': (float, type(None)),
|
||||
}
|
||||
|
||||
_LIST_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = {
|
||||
'List[int]': (int, ),
|
||||
'List[str]': (str, ),
|
||||
'List[bool]': (bool, ),
|
||||
'List[float]': (float, ),
|
||||
T = TypeVar('T')
|
||||
|
||||
SIMPLE_NAMES_TO_TYPES: Dict[str, Type] = {
|
||||
'int': int,
|
||||
'bool': bool,
|
||||
'str': str,
|
||||
'float': float,
|
||||
}
|
||||
SIMPLE_TYPES_TO_NAMES = {tp: nm for nm, tp in SIMPLE_NAMES_TO_TYPES.items()}
|
||||
|
||||
|
||||
def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None:
|
||||
"""Safely assign values from a dict to a dataclass instance.
|
||||
def dataclass_to_dict(obj: Any, coerce_to_float: bool = True) -> dict:
|
||||
"""Given a dataclass object, emit a json-friendly dict.
|
||||
|
||||
A TypeError will be raised if types to not match the dataclass fields
|
||||
or are unsupported by this function. Note that a limited number of
|
||||
types are supported. More can be added as needed.
|
||||
All values will be checked to ensure they match the types specified
|
||||
on fields. Note that only a limited set of types is supported.
|
||||
|
||||
Exact types are strictly checked, so a bool cannot be passed for
|
||||
an int field, an int can't be passed for a float, etc.
|
||||
(can reexamine this strictness if it proves to be a problem)
|
||||
|
||||
An AttributeError will be raised if attributes are passed which are
|
||||
not present on the dataclass as fields.
|
||||
|
||||
This function may add significant overhead compared to passing dict
|
||||
values to a dataclass' constructor or other more direct methods, but
|
||||
the increased safety checks may be worth the speed tradeoff in some
|
||||
cases.
|
||||
If coerce_to_float is True, integer values present on float typed fields
|
||||
will be converted to floats in the dict output. If False, a TypeError
|
||||
will be triggered.
|
||||
"""
|
||||
_dataclass_validate(instance, values)
|
||||
for key, value in values.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
out = _Outputter(obj, create=True, coerce_to_float=coerce_to_float).run()
|
||||
assert isinstance(out, dict)
|
||||
return out
|
||||
|
||||
|
||||
def dataclass_validate(instance: Any) -> None:
|
||||
"""Ensure values in a dataclass are correct types.
|
||||
def dataclass_from_dict(cls: Type[T],
|
||||
values: dict,
|
||||
coerce_to_float: bool = True) -> T:
|
||||
"""Given a dict, instantiates a dataclass of the given type.
|
||||
|
||||
Note that this will always fail if a dataclass contains field types
|
||||
not supported by this module.
|
||||
The dict must be in the json-friendly format as emitted from
|
||||
dataclass_to_dict. This means that sequence values such as tuples or
|
||||
sets should be passed as lists, enums should be passed as their
|
||||
associated values, and nested dataclasses should be passed as dicts.
|
||||
|
||||
If coerce_to_float is True, int values passed for float typed fields
|
||||
will be converted to float values. Otherwise a TypeError is raised.
|
||||
"""
|
||||
_dataclass_validate(instance, dataclasses.asdict(instance))
|
||||
return _Inputter(cls, coerce_to_float=coerce_to_float).run(values)
|
||||
|
||||
|
||||
def _dataclass_validate(instance: Any, values: Dict[str, Any]) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
if not dataclasses.is_dataclass(instance):
|
||||
raise TypeError(f'Passed instance {instance} is not a dataclass.')
|
||||
if not isinstance(values, dict):
|
||||
raise TypeError("Expected a dict for 'values' arg.")
|
||||
fields = dataclasses.fields(instance)
|
||||
fieldsdict = {f.name: f for f in fields}
|
||||
for key, value in values.items():
|
||||
if key not in fieldsdict:
|
||||
raise AttributeError(
|
||||
f"'{type(instance).__name__}' has no '{key}' field.")
|
||||
field = fieldsdict[key]
|
||||
def dataclass_validate(obj: Any, coerce_to_float: bool = True) -> None:
|
||||
"""Ensure that current values in a dataclass are the correct types."""
|
||||
_Outputter(obj, create=False, coerce_to_float=coerce_to_float).run()
|
||||
|
||||
# We expect to be operating under 'from __future__ import annotations'
|
||||
# so field types should always be strings for us; not an actual types.
|
||||
# Complain if we come across an actual type.
|
||||
fieldtype: str = field.type # type: ignore
|
||||
if not isinstance(fieldtype, str):
|
||||
raise RuntimeError(
|
||||
f'Dataclass {type(instance).__name__} seems to have'
|
||||
f' been created without "from __future__ import annotations";'
|
||||
f' those dataclasses are unsupported here.')
|
||||
|
||||
if fieldtype in _SIMPLE_ASSIGN_TYPES:
|
||||
reqtypes = _SIMPLE_ASSIGN_TYPES[fieldtype]
|
||||
valuetype = type(value)
|
||||
if not any(valuetype is t for t in reqtypes):
|
||||
if len(reqtypes) == 1:
|
||||
expected = reqtypes[0].__name__
|
||||
else:
|
||||
names = ', '.join(t.__name__ for t in reqtypes)
|
||||
expected = f'Union[{names}]'
|
||||
raise TypeError(f'Invalid value type for "{key}";'
|
||||
f' expected "{expected}", got'
|
||||
f' "{valuetype.__name__}".')
|
||||
def _field_type_str(cls: Type, field: dataclasses.Field) -> str:
|
||||
# We expect to be operating under 'from __future__ import annotations'
|
||||
# so field types should always be strings for us; not actual types.
|
||||
# (Can pull this check out once we get to Python 3.10)
|
||||
typestr: str = field.type # type: ignore
|
||||
|
||||
elif fieldtype in _LIST_ASSIGN_TYPES:
|
||||
reqtypes = _LIST_ASSIGN_TYPES[fieldtype]
|
||||
if not isinstance(typestr, str):
|
||||
raise RuntimeError(
|
||||
f'Dataclass {cls.__name__} seems to have'
|
||||
f' been created without "from __future__ import annotations";'
|
||||
f' those dataclasses are unsupported here.')
|
||||
return typestr
|
||||
|
||||
|
||||
def _raise_type_error(fieldpath: str, valuetype: Type,
|
||||
expected: Tuple[Type, ...]) -> None:
|
||||
"""Raise an error when a field value's type does not match expected."""
|
||||
assert isinstance(expected, tuple)
|
||||
assert all(isinstance(e, type) for e in expected)
|
||||
if len(expected) == 1:
|
||||
expected_str = expected[0].__name__
|
||||
else:
|
||||
names = ', '.join(t.__name__ for t in expected)
|
||||
expected_str = f'Union[{names}]'
|
||||
raise TypeError(f'Invalid value type for "{fieldpath}";'
|
||||
f' expected "{expected_str}", got'
|
||||
f' "{valuetype.__name__}".')
|
||||
|
||||
|
||||
class _Outputter:
|
||||
|
||||
def __init__(self, obj: Any, create: bool, coerce_to_float: bool) -> None:
|
||||
self._obj = obj
|
||||
self._create = create
|
||||
self._coerce_to_float = coerce_to_float
|
||||
|
||||
def run(self) -> Any:
|
||||
"""Do the thing."""
|
||||
return self._dataclass_to_output(self._obj, '')
|
||||
|
||||
def _value_to_output(self, fieldpath: str, typestr: str,
|
||||
value: Any) -> Any:
|
||||
# pylint: disable=too-many-return-statements
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
# For simple flat types, look for exact matches:
|
||||
simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr)
|
||||
if simpletype is not None:
|
||||
if type(value) is not simpletype:
|
||||
# Special case: if they want to coerce ints to floats, do so.
|
||||
if (self._coerce_to_float and simpletype is float
|
||||
and type(value) is int):
|
||||
return float(value) if self._create else None
|
||||
_raise_type_error(fieldpath, type(value), (simpletype, ))
|
||||
return value
|
||||
|
||||
if typestr.startswith('Optional[') and typestr.endswith(']'):
|
||||
subtypestr = typestr[9:-1]
|
||||
# Handle the 'None' case special and do the default otherwise.
|
||||
if value is None:
|
||||
return None
|
||||
return self._value_to_output(fieldpath, subtypestr, value)
|
||||
|
||||
if typestr.startswith('List[') and typestr.endswith(']'):
|
||||
subtypestr = typestr[5:-1]
|
||||
if not isinstance(value, list):
|
||||
raise TypeError(
|
||||
f'Invalid value for "{key}";'
|
||||
f' expected a list, got a "{type(value).__name__}"')
|
||||
for subvalue in value:
|
||||
subvaluetype = type(subvalue)
|
||||
if not any(subvaluetype is t for t in reqtypes):
|
||||
if len(reqtypes) == 1:
|
||||
expected = reqtypes[0].__name__
|
||||
else:
|
||||
names = ', '.join(t.__name__ for t in reqtypes)
|
||||
expected = f'Union[{names}]'
|
||||
raise TypeError(f'Invalid value type for "{key}";'
|
||||
f' expected list of "{expected}", found'
|
||||
f' "{subvaluetype.__name__}".')
|
||||
raise TypeError(f'Expected a list for {fieldpath};'
|
||||
f' found a {type(value)}')
|
||||
if self._create:
|
||||
return [
|
||||
self._value_to_output(fieldpath, subtypestr, x)
|
||||
for x in value
|
||||
]
|
||||
for x in value:
|
||||
self._value_to_output(fieldpath, subtypestr, x)
|
||||
return None
|
||||
|
||||
else:
|
||||
raise TypeError(f'Field type "{fieldtype}" is unsupported here.')
|
||||
if typestr.startswith('Set[') and typestr.endswith(']'):
|
||||
subtypestr = typestr[4:-1]
|
||||
if not isinstance(value, set):
|
||||
raise TypeError(f'Expected a set for {fieldpath};'
|
||||
f' found a {type(value)}')
|
||||
if self._create:
|
||||
# Note: we output json-friendly values so this becomes a list.
|
||||
return [
|
||||
self._value_to_output(fieldpath, subtypestr, x)
|
||||
for x in value
|
||||
]
|
||||
for x in value:
|
||||
self._value_to_output(fieldpath, subtypestr, x)
|
||||
return None
|
||||
|
||||
if dataclasses.is_dataclass(value):
|
||||
return self._dataclass_to_output(value, fieldpath)
|
||||
|
||||
if isinstance(value, Enum):
|
||||
enumvalue = value.value
|
||||
if type(enumvalue) not in SIMPLE_TYPES_TO_NAMES:
|
||||
raise TypeError(f'Invalid enum value type {type(enumvalue)}'
|
||||
f' for "{fieldpath}".')
|
||||
return enumvalue
|
||||
|
||||
raise TypeError(
|
||||
f"Field '{fieldpath}' of type '{typestr}' is unsupported here.")
|
||||
|
||||
def _dataclass_to_output(self, obj: Any, fieldpath: str) -> Any:
|
||||
if not dataclasses.is_dataclass(obj):
|
||||
raise TypeError(f'Passed obj {obj} is not a dataclass.')
|
||||
fields = dataclasses.fields(obj)
|
||||
out: Optional[Dict[str, Any]] = {} if self._create else None
|
||||
|
||||
for field in fields:
|
||||
fieldname = field.name
|
||||
|
||||
if fieldpath:
|
||||
subfieldpath = f'{fieldpath}.{fieldname}'
|
||||
else:
|
||||
subfieldpath = fieldname
|
||||
typestr = _field_type_str(type(obj), field)
|
||||
value = getattr(obj, fieldname)
|
||||
outvalue = self._value_to_output(subfieldpath, typestr, value)
|
||||
if self._create:
|
||||
assert out is not None
|
||||
out[fieldname] = outvalue
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class _Inputter(Generic[T]):
|
||||
|
||||
def __init__(self, cls: Type[T], coerce_to_float: bool):
|
||||
self._cls = cls
|
||||
self._coerce_to_float = coerce_to_float
|
||||
|
||||
def run(self, values: dict) -> T:
|
||||
"""Do the thing."""
|
||||
return self._dataclass_from_input( # type: ignore
|
||||
self._cls, '', values)
|
||||
|
||||
def _value_from_input(self, cls: Type, fieldpath: str, typestr: str,
|
||||
value: Any) -> Any:
|
||||
"""Convert an assigned value to what a dataclass field expects."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr)
|
||||
if simpletype is not None:
|
||||
if type(value) is not simpletype:
|
||||
# Special case: if they want to coerce ints to floats, do so.
|
||||
if (self._coerce_to_float and simpletype is float
|
||||
and type(value) is int):
|
||||
return float(value)
|
||||
_raise_type_error(fieldpath, type(value), (simpletype, ))
|
||||
return value
|
||||
if typestr.startswith('List[') and typestr.endswith(']'):
|
||||
return self._sequence_from_input(cls, fieldpath, typestr, value,
|
||||
'List', list)
|
||||
if typestr.startswith('Set[') and typestr.endswith(']'):
|
||||
return self._sequence_from_input(cls, fieldpath, typestr, value,
|
||||
'Set', set)
|
||||
if typestr.startswith('Optional[') and typestr.endswith(']'):
|
||||
subtypestr = typestr[9:-1]
|
||||
# Handle the 'None' case special and do the default
|
||||
# thing otherwise.
|
||||
if value is None:
|
||||
return None
|
||||
return self._value_from_input(cls, fieldpath, subtypestr, value)
|
||||
|
||||
# Ok, its not a builtin type. It might be an enum or nested dataclass.
|
||||
cls2 = getattr(inspect.getmodule(cls), typestr, None)
|
||||
if cls2 is None:
|
||||
raise RuntimeError(f"Unable to resolve '{typestr}'"
|
||||
f" used by class '{cls.__name__}';"
|
||||
f' make sure all nested types are declared'
|
||||
f' in the global namespace of the module where'
|
||||
f" '{cls.__name__} is defined.")
|
||||
|
||||
if dataclasses.is_dataclass(cls2):
|
||||
return self._dataclass_from_input(cls2, fieldpath, value)
|
||||
|
||||
if issubclass(cls2, Enum):
|
||||
return enum_by_value(cls2, value)
|
||||
|
||||
raise TypeError(
|
||||
f"Field '{fieldpath}' of type '{typestr}' is unsupported here.")
|
||||
|
||||
def _dataclass_from_input(self, cls: Type, fieldpath: str,
|
||||
values: dict) -> Any:
|
||||
"""Given a dict, instantiates a dataclass of the given type.
|
||||
|
||||
The dict must be in the json-friendly format as emitted from
|
||||
dataclass_to_dict. This means that sequence values such as tuples or
|
||||
sets should be passed as lists, enums should be passed as their
|
||||
associated values, and nested dataclasses should be passed as dicts.
|
||||
"""
|
||||
if not dataclasses.is_dataclass(cls):
|
||||
raise TypeError(f'Passed class {cls} is not a dataclass.')
|
||||
if not isinstance(values, dict):
|
||||
raise TypeError("Expected a dict for 'values' arg.")
|
||||
|
||||
# noinspection PyDataclass
|
||||
fields = dataclasses.fields(cls)
|
||||
fields_by_name = {f.name: f for f in fields}
|
||||
args: Dict[str, Any] = {}
|
||||
for key, value in values.items():
|
||||
field = fields_by_name.get(key)
|
||||
if field is None:
|
||||
raise AttributeError(f"'{cls.__name__}' has no '{key}' field.")
|
||||
|
||||
typestr = _field_type_str(cls, field)
|
||||
|
||||
subfieldpath = (f'{fieldpath}.{field.name}'
|
||||
if fieldpath else field.name)
|
||||
args[key] = self._value_from_input(cls, subfieldpath, typestr,
|
||||
value)
|
||||
|
||||
return cls(**args)
|
||||
|
||||
def _sequence_from_input(self, cls: Type, fieldpath: str, typestr: str,
|
||||
value: Any, seqtypestr: str,
|
||||
seqtype: Type) -> Any:
|
||||
# Because we are json-centric, we expect a list for all sequences.
|
||||
if type(value) is not list:
|
||||
raise TypeError(f'Invalid input value for "{fieldpath}";'
|
||||
f' expected a list, got a {type(value).__name__}')
|
||||
subtypestr = typestr[len(seqtypestr) + 1:-1]
|
||||
return seqtype(
|
||||
self._value_from_input(cls, fieldpath, subtypestr, i)
|
||||
for i in value)
|
||||
|
||||
@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import time
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, cast, TypeVar, Generic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -18,12 +19,38 @@ T = TypeVar('T')
|
||||
TVAL = TypeVar('TVAL')
|
||||
TARG = TypeVar('TARG')
|
||||
TRET = TypeVar('TRET')
|
||||
TENUM = TypeVar('TENUM', bound=Enum)
|
||||
|
||||
|
||||
class _EmptyObj:
|
||||
pass
|
||||
|
||||
|
||||
def enum_by_value(cls: Type[TENUM], value: Any) -> TENUM:
|
||||
"""Create an enum from a value.
|
||||
|
||||
This is basically the same as doing 'obj = EnumType(value)' except
|
||||
that it works around an issue where a reference loop is created
|
||||
if an exception is thrown due to an invalid value. Since we disable
|
||||
the cyclic garbage collector for most of the time, such loops can lead
|
||||
to our objects sticking around longer than we want.
|
||||
This issue has been submitted to Python as a bug so hopefully we can
|
||||
remove this eventually if it gets fixed: https://bugs.python.org/issue42248
|
||||
"""
|
||||
|
||||
# Note: we don't recreate *ALL* the functionality of the Enum constructor
|
||||
# such as the _missing_ hook; but this should cover our basic needs.
|
||||
value2member_map = getattr(cls, '_value2member_map_')
|
||||
assert value2member_map is not None
|
||||
try:
|
||||
out = value2member_map[value]
|
||||
assert isinstance(out, cls)
|
||||
return out
|
||||
except KeyError:
|
||||
raise ValueError('%r is not a valid %s' %
|
||||
(value, cls.__name__)) from None
|
||||
|
||||
|
||||
def utc_now() -> datetime.datetime:
|
||||
"""Get offset-aware current utc time.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user