mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-04 22:43:17 +08:00
Wired up server config.yaml functionality
This commit is contained in:
parent
b42fc09ffc
commit
48f72ec123
14
.idea/dictionaries/ericf.xml
generated
14
.idea/dictionaries/ericf.xml
generated
@ -165,6 +165,7 @@
|
||||
<w>bgthread</w>
|
||||
<w>bhval</w>
|
||||
<w>binc</w>
|
||||
<w>bincfg</w>
|
||||
<w>bindcode</w>
|
||||
<w>bindvals</w>
|
||||
<w>bisectmodule</w>
|
||||
@ -512,6 +513,7 @@
|
||||
<w>efrotool</w>
|
||||
<w>efrotools</w>
|
||||
<w>eftools</w>
|
||||
<w>efxjtp</w>
|
||||
<w>eids</w>
|
||||
<w>elementtree</w>
|
||||
<w>elim</w>
|
||||
@ -588,6 +590,8 @@
|
||||
<w>fhashes</w>
|
||||
<w>fhdr</w>
|
||||
<w>fieldattr</w>
|
||||
<w>fieldsdict</w>
|
||||
<w>fieldtype</w>
|
||||
<w>fieldtypes</w>
|
||||
<w>filebytes</w>
|
||||
<w>filecmp</w>
|
||||
@ -941,6 +945,7 @@
|
||||
<w>lazybuild</w>
|
||||
<w>lazybuilddir</w>
|
||||
<w>lbits</w>
|
||||
<w>lbld</w>
|
||||
<w>lcfg</w>
|
||||
<w>lcolor</w>
|
||||
<w>lcrypto</w>
|
||||
@ -1208,13 +1213,16 @@
|
||||
<w>objs</w>
|
||||
<w>objt</w>
|
||||
<w>objtype</w>
|
||||
<w>obval</w>
|
||||
<w>occurrances</w>
|
||||
<w>oculus</w>
|
||||
<w>offsanchor</w>
|
||||
<w>ofval</w>
|
||||
<w>oggenc</w>
|
||||
<w>oghash</w>
|
||||
<w>oghashes</w>
|
||||
<w>ogval</w>
|
||||
<w>oival</w>
|
||||
<w>oldlady</w>
|
||||
<w>onln</w>
|
||||
<w>onscreencountdown</w>
|
||||
@ -1234,6 +1242,7 @@
|
||||
<w>osascript</w>
|
||||
<w>osmusic</w>
|
||||
<w>ostype</w>
|
||||
<w>osval</w>
|
||||
<w>otherplayer</w>
|
||||
<w>otherspawn</w>
|
||||
<w>ourself</w>
|
||||
@ -1473,6 +1482,8 @@
|
||||
<w>representer</w>
|
||||
<w>reprlib</w>
|
||||
<w>reqs</w>
|
||||
<w>reqtype</w>
|
||||
<w>reqtypes</w>
|
||||
<w>resample</w>
|
||||
<w>resourcetypeinfo</w>
|
||||
<w>respawn</w>
|
||||
@ -1573,6 +1584,7 @@
|
||||
<w>shiftposition</w>
|
||||
<w>shouldn</w>
|
||||
<w>showpoints</w>
|
||||
<w>showstats</w>
|
||||
<w>showsubseconds</w>
|
||||
<w>shroom</w>
|
||||
<w>shutil</w>
|
||||
@ -1720,6 +1732,7 @@
|
||||
<w>tbutton</w>
|
||||
<w>tcall</w>
|
||||
<w>tchar</w>
|
||||
<w>tclass</w>
|
||||
<w>tcombine</w>
|
||||
<w>tdelay</w>
|
||||
<w>tdval</w>
|
||||
@ -1833,6 +1846,7 @@
|
||||
<w>txtval</w>
|
||||
<w>txtw</w>
|
||||
<w>typeargs</w>
|
||||
<w>typecheck</w>
|
||||
<w>typechecker</w>
|
||||
<w>typedval</w>
|
||||
<w>typeshed</w>
|
||||
|
||||
6
Makefile
6
Makefile
@ -596,8 +596,12 @@ test-assetmanager:
|
||||
@tools/snippets pytest -o log_cli=true -o log_cli_level=debug -s -v \
|
||||
tests/test_ba/test_assetmanager.py::test_assetmanager
|
||||
|
||||
test-dataclassutils:
|
||||
@tools/snippets pytest -o log_cli=true -o log_cli_level=debug -s -v \
|
||||
tests/test_efro/test_dataclassutils.py
|
||||
|
||||
# Tell make which of these targets don't represent files.
|
||||
.PHONY: test test-full
|
||||
.PHONY: test test-full test-assetmanager
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
@ -453,9 +453,11 @@
|
||||
"ba_data/python/bastd/ui/watch.py",
|
||||
"ba_data/python/efro/__init__.py",
|
||||
"ba_data/python/efro/__pycache__/__init__.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/jsonutils.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/util.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/dataclassutils.py",
|
||||
"ba_data/python/efro/entity/__init__.py",
|
||||
"ba_data/python/efro/entity/__pycache__/__init__.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/efro/entity/__pycache__/_base.cpython-37.opt-1.pyc",
|
||||
|
||||
@ -144,6 +144,7 @@ ASSET_TARGETS_WIN_X64 += $(EXTRAS_TARGETS_WIN_X64)
|
||||
SCRIPT_TARGETS_PY_1 = \
|
||||
build/server/ballisticacore_server.py \
|
||||
build/ba_data/python/efro/executils.py \
|
||||
build/ba_data/python/efro/dataclassutils.py \
|
||||
build/ba_data/python/efro/util.py \
|
||||
build/ba_data/python/efro/__init__.py \
|
||||
build/ba_data/python/efro/jsonutils.py \
|
||||
@ -384,6 +385,7 @@ SCRIPT_TARGETS_PY_1 = \
|
||||
SCRIPT_TARGETS_PYC_1 = \
|
||||
build/server/__pycache__/ballisticacore_server.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/util.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/__init__.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/efro/__pycache__/jsonutils.cpython-37.opt-1.pyc \
|
||||
@ -644,6 +646,11 @@ build/ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc: \
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/efro/dataclassutils.py
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/efro/__pycache__/util.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/efro/util.py
|
||||
@echo Compiling script: $^
|
||||
|
||||
@ -104,8 +104,8 @@ class ServerConfig:
|
||||
# this to provide a convenient in-game link to it in the server-browser
|
||||
# beside the server name.
|
||||
# if ${ACCOUNT} is present in the string, it will be replaced by the
|
||||
# currently-signed-in account's id. To get info about an account,
|
||||
# you can use the following url:
|
||||
# currently-signed-in account's id. To fetch info about an account,
|
||||
# your backend server can use the following url:
|
||||
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
|
||||
stats_url: Optional[str] = None
|
||||
|
||||
|
||||
116
assets/src/ba_data/python/efro/dataclassutils.py
Normal file
116
assets/src/ba_data/python/efro/dataclassutils.py
Normal file
@ -0,0 +1,116 @@
|
||||
# Copyright (c) 2011-2020 Eric Froemling
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# -----------------------------------------------------------------------------
|
||||
"""Custom functionality for dealing with dataclasses."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Type, Tuple
|
||||
|
||||
# For fields with these type strings, we require a passed value's type
|
||||
# to exactly match one of the tuple values to consider the assignment valid.
|
||||
_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = {
|
||||
'bool': (bool, ),
|
||||
'str': (str, ),
|
||||
'int': (int, ),
|
||||
'float': (float, ),
|
||||
'Optional[int]': (int, type(None)),
|
||||
'Optional[str]': (str, type(None)),
|
||||
'Optional[bool]': (bool, type(None)),
|
||||
'Optional[float]': (float, type(None)),
|
||||
}
|
||||
|
||||
|
||||
def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None:
|
||||
"""Safely assign values from a dict to a dataclass instance.
|
||||
|
||||
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.
|
||||
|
||||
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 be significantly slower than simply passing dict
|
||||
values to a dataclass' constructor or other more direct methods, but
|
||||
the increased safety checks may be worth the extra overhead in some
|
||||
cases.
|
||||
"""
|
||||
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__}' dataclass has"
|
||||
f" no '{key}' field.")
|
||||
field = fieldsdict[key]
|
||||
|
||||
# We expect to be operating with 'from __future__ import annotations'
|
||||
# so this should always be a string for us; not an actual type.
|
||||
# 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.')
|
||||
|
||||
reqtypes = _ASSIGN_TYPES.get(fieldtype)
|
||||
if reqtypes is not None:
|
||||
# pylint: disable=unidiomatic-typecheck
|
||||
if not any(type(value) is t for t in reqtypes):
|
||||
# if not isinstance(value, reqtype):
|
||||
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' "{type(value).__name__}".')
|
||||
else:
|
||||
raise TypeError(f'Field type "{fieldtype}" is unsupported here.')
|
||||
|
||||
# Ok, if we made it here, the value is kosher. Do the assign.
|
||||
setattr(instance, key, value)
|
||||
|
||||
|
||||
def dataclass_validate(instance: Any) -> None:
|
||||
"""Ensure values in a dataclass are correct types.
|
||||
|
||||
Note that this will always fail if a dataclass has value types
|
||||
unsupported by this module.
|
||||
"""
|
||||
# We currently simply operate by grabbing dataclass values as a dict
|
||||
# and passing them through dataclass_assign().
|
||||
# It would be possible to write slightly more efficient custom code,
|
||||
# this, but this keeps things simple and will allow us to easily
|
||||
# incorporate things like value coercion later if we add that.
|
||||
dataclass_assign(instance, dataclasses.asdict(instance))
|
||||
@ -30,8 +30,8 @@ if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
# Special attr we included for our extended type information
|
||||
# (extended-json-type)
|
||||
TYPE_TAG = '_xjtp'
|
||||
# (efro-extended-json-type)
|
||||
TYPE_TAG = '_efxjtp'
|
||||
|
||||
_pytz_utc: Any
|
||||
|
||||
|
||||
@ -31,8 +31,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# We make use of the bacommon package and site-packages included
|
||||
# with our bundled Ballistica dist.
|
||||
# We make use of the bacommon and efro packages as well as site-packages
|
||||
# included with our bundled Ballistica dist.
|
||||
sys.path += [
|
||||
str(Path(os.getcwd(), 'dist', 'ba_data', 'python')),
|
||||
str(Path(os.getcwd(), 'dist', 'ba_data', 'python-site-packages'))
|
||||
@ -40,6 +40,7 @@ sys.path += [
|
||||
|
||||
from bacommon.servermanager import (ServerConfig, ServerCommand,
|
||||
make_server_command)
|
||||
from efro.dataclassutils import dataclass_assign
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, List, Dict
|
||||
@ -55,13 +56,14 @@ class ServerManagerApp:
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
# We actually run from the 'dist' subdir.
|
||||
self._config = self._load_config()
|
||||
|
||||
# We actually operate from the 'dist' subdir.
|
||||
if not os.path.isdir('dist'):
|
||||
raise RuntimeError('"dist" directory not found.')
|
||||
os.chdir('dist')
|
||||
|
||||
self._binary_path = self._get_binary_path()
|
||||
self._config = ServerConfig()
|
||||
|
||||
self._binary_commands: List[str] = []
|
||||
self._binary_commands_lock = threading.Lock()
|
||||
@ -75,6 +77,31 @@ class ServerManagerApp:
|
||||
self._process: Optional[subprocess.Popen[bytes]] = None
|
||||
self._process_launch_time: Optional[float] = None
|
||||
|
||||
@property
|
||||
def config(self) -> ServerConfig:
|
||||
"""The current config settings for the app."""
|
||||
return self._config
|
||||
|
||||
def _load_config(self) -> ServerConfig:
|
||||
user_config_path = 'config.yaml'
|
||||
|
||||
# Start with a default config, and if there is a config.yaml,
|
||||
# override parts of it.
|
||||
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())
|
||||
dataclass_assign(config, user_config)
|
||||
|
||||
# An empty config file will yield None, and that's ok.
|
||||
if user_config is not None:
|
||||
if not isinstance(user_config, dict):
|
||||
raise RuntimeError(f'Invalid config format; expected dict,'
|
||||
f' got {type(user_config)}.')
|
||||
|
||||
return config
|
||||
|
||||
def _get_binary_path(self) -> str:
|
||||
"""Locate the game binary that we'll use."""
|
||||
if os.name == 'nt':
|
||||
@ -215,15 +242,15 @@ class ServerManagerApp:
|
||||
os.makedirs('ba_root', exist_ok=True)
|
||||
if os.path.exists('ba_root/config.json'):
|
||||
with open('ba_root/config.json') as infile:
|
||||
bacfg = json.loads(infile.read())
|
||||
bincfg = json.loads(infile.read())
|
||||
else:
|
||||
bacfg = {}
|
||||
bacfg['Port'] = self._config.port
|
||||
bacfg['Enable Telnet'] = self._config.enable_telnet
|
||||
bacfg['Telnet Port'] = self._config.telnet_port
|
||||
bacfg['Telnet Password'] = self._config.telnet_password
|
||||
bincfg = {}
|
||||
bincfg['Port'] = self._config.port
|
||||
bincfg['Enable Telnet'] = self._config.enable_telnet
|
||||
bincfg['Telnet Port'] = self._config.telnet_port
|
||||
bincfg['Telnet Password'] = self._config.telnet_password
|
||||
with open('ba_root/config.json', 'w') as outfile:
|
||||
outfile.write(json.dumps(bacfg))
|
||||
outfile.write(json.dumps(bincfg))
|
||||
|
||||
def _run_process_until_exit(self) -> None:
|
||||
assert self._process is not None
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2020-04-29 for Ballistica version 1.5.0 build 20001</em></h4>
|
||||
<h4><em>last updated on 2020-04-30 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>
|
||||
|
||||
155
tests/test_efro/test_dataclassutils.py
Normal file
155
tests/test_efro/test_dataclassutils.py
Normal file
@ -0,0 +1,155 @@
|
||||
# Copyright (c) 2011-2020 Eric Froemling
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# -----------------------------------------------------------------------------
|
||||
"""Testing dataclassutils functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from efro.dataclassutils import dataclass_assign, dataclass_validate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def test_assign() -> None:
|
||||
"""Testing various assignments."""
|
||||
|
||||
@dataclass
|
||||
class _TestClass:
|
||||
ival: int = 0
|
||||
sval: str = ''
|
||||
bval: bool = True
|
||||
fval: float = 1.0
|
||||
oival: Optional[int] = None
|
||||
osval: Optional[str] = None
|
||||
obval: Optional[bool] = None
|
||||
ofval: Optional[float] = None
|
||||
|
||||
tclass = _TestClass()
|
||||
|
||||
class _TestClass2:
|
||||
pass
|
||||
|
||||
tclass2 = _TestClass2()
|
||||
|
||||
# Arg types:
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass2, {})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, []) # type: ignore
|
||||
|
||||
# Invalid attrs.
|
||||
with pytest.raises(AttributeError):
|
||||
dataclass_assign(tclass, {'nonexistent': 'foo'})
|
||||
|
||||
# Correct types.
|
||||
dataclass_assign(tclass, {
|
||||
'ival': 1,
|
||||
'sval': 'foo',
|
||||
'bval': True,
|
||||
'fval': 2.0,
|
||||
})
|
||||
dataclass_assign(tclass, {
|
||||
'oival': None,
|
||||
'osval': None,
|
||||
'obval': None,
|
||||
'ofval': None,
|
||||
})
|
||||
dataclass_assign(tclass, {
|
||||
'oival': 1,
|
||||
'osval': 'foo',
|
||||
'obval': True,
|
||||
'ofval': 2.0,
|
||||
})
|
||||
|
||||
# Type mismatches.
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ival': 'foo'})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'sval': 1})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'bval': 2})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'oival': 'foo'})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'osval': 1})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'obval': 2})
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_assign(tclass, {'ofval': 'blah'})
|
||||
|
||||
# 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})
|
||||
|
||||
|
||||
def test_validate() -> None:
|
||||
"""Testing validation."""
|
||||
|
||||
@dataclass
|
||||
class _TestClass:
|
||||
ival: int = 0
|
||||
sval: str = ''
|
||||
bval: bool = True
|
||||
fval: float = 1.0
|
||||
oival: Optional[int] = None
|
||||
osval: Optional[str] = None
|
||||
obval: Optional[bool] = None
|
||||
ofval: Optional[float] = None
|
||||
|
||||
# Should pass by default.
|
||||
tclass = _TestClass()
|
||||
dataclass_validate(tclass)
|
||||
|
||||
# No longer valid.
|
||||
tclass.fval = 1
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_validate(tclass)
|
||||
|
||||
# Should pass by default.
|
||||
tclass = _TestClass()
|
||||
dataclass_validate(tclass)
|
||||
|
||||
# No longer valid.
|
||||
# noinspection PyTypeHints
|
||||
tclass.ival = None # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
dataclass_validate(tclass)
|
||||
@ -615,11 +615,17 @@ def _get_server_config_template_yaml() -> str:
|
||||
assert veq == '='
|
||||
vval = eval(vval_raw) # pylint: disable=eval-used
|
||||
|
||||
# Override a few specifics:
|
||||
# Filter/override a few things.
|
||||
if vname == 'playlist_code':
|
||||
# User wouldn't want to pass the default of None here.
|
||||
vval = 12345
|
||||
elif vname == 'stats_url':
|
||||
vval = ('https://mystatssite.com/'
|
||||
'showstats?player=${ACCOUNT}')
|
||||
lines_out.append('#' + yaml.dump({vname: vval}).strip())
|
||||
else:
|
||||
# Convert comments referring to python bools to yaml bools.
|
||||
line = line.replace('True', 'true').replace('False', 'false')
|
||||
lines_out.append(line)
|
||||
return '\n'.join(lines_out)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user