kopia lustrzana https://github.com/corrscope/corrscope
Add Alias class, for config field aliasing
rodzic
acffc1a55e
commit
56438bed5c
|
@ -8,6 +8,7 @@ from ruamel.yaml import yaml_object, YAML, Representer
|
|||
if TYPE_CHECKING:
|
||||
from enum import Enum
|
||||
|
||||
# Setup YAML loading (yaml object).
|
||||
|
||||
class MyYAML(YAML):
|
||||
def dump(self, data, stream=None, **kw):
|
||||
|
@ -28,10 +29,12 @@ yaml = MyYAML()
|
|||
_yaml_loadable = yaml_object(yaml)
|
||||
|
||||
|
||||
# Setup configuration load/dump infrastructure.
|
||||
|
||||
def register_config(cls=None, *, always_dump: str = ''):
|
||||
""" Marks class as @dataclass, and enables YAML dumping (excludes default fields).
|
||||
|
||||
dataclasses.dataclass is compatible with yaml.register_class.
|
||||
dataclasses.dataclass is compatible with yaml_object().
|
||||
typing.NamedTuple is incompatible.
|
||||
"""
|
||||
|
||||
|
@ -49,14 +52,6 @@ def register_config(cls=None, *, always_dump: str = ''):
|
|||
else:
|
||||
return decorator
|
||||
|
||||
# __init__-less non-dataclasses are also compatible with yaml.register_class.
|
||||
|
||||
|
||||
# Default value for dataclass field
|
||||
def default(value):
|
||||
string = repr(value)
|
||||
return field(default=lambda: eval(string))
|
||||
|
||||
|
||||
@dataclass()
|
||||
class _ConfigMixin:
|
||||
|
@ -69,7 +64,7 @@ class _ConfigMixin:
|
|||
|
||||
# SafeRepresenter.represent_yaml_object() uses __getstate__ to dump objects.
|
||||
def __getstate__(self):
|
||||
""" Returns all fields with non-default value, or appeear in
|
||||
""" Removes all fields with default values, but not found in
|
||||
self.always_dump. """
|
||||
|
||||
always_dump = set(self.always_dump.split())
|
||||
|
@ -94,10 +89,47 @@ class _ConfigMixin:
|
|||
|
||||
# SafeConstructor.construct_yaml_object() uses __setstate__ to load objects.
|
||||
def __setstate__(self, state):
|
||||
"""Call the dataclass constructor, to ensure all parameters are valid."""
|
||||
new = type(self)(**state)
|
||||
self.__dict__ = new.__dict__
|
||||
""" Redirect `Alias(key)=value` to `key=value`.
|
||||
Then call the dataclass constructor (to validate parameters). """
|
||||
|
||||
for key, value in dict(state).items():
|
||||
classvar = getattr(self, key, None)
|
||||
if not isinstance(classvar, Alias):
|
||||
continue
|
||||
|
||||
target = classvar.key
|
||||
if target in state:
|
||||
raise TypeError(
|
||||
f'{type(self).__name__} received both Alias {key} and equivalent '
|
||||
f'{target}'
|
||||
)
|
||||
|
||||
state[target] = value
|
||||
del state[key]
|
||||
|
||||
obj = type(self)(**state)
|
||||
self.__dict__ = obj.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Alias:
|
||||
"""
|
||||
@register_config
|
||||
class Foo:
|
||||
x: int
|
||||
xx = Alias('x') # do not add a type hint
|
||||
"""
|
||||
key: str
|
||||
|
||||
|
||||
# Unused
|
||||
def default(value):
|
||||
"""Supplies a mutable default value for a dataclass field."""
|
||||
string = repr(value)
|
||||
return field(default=lambda: eval(string))
|
||||
|
||||
|
||||
# Setup Enum load/dump infrastructure
|
||||
|
||||
def register_enum(cls: type):
|
||||
cls.to_yaml = _EnumMixin.to_yaml
|
||||
|
@ -110,6 +142,8 @@ class _EnumMixin:
|
|||
return representer.represent_str(node._name_)
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
class OvgenError(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
@ -4,11 +4,14 @@ import sys
|
|||
import pytest
|
||||
from ruamel.yaml import yaml_object
|
||||
|
||||
from ovgenpy.config import register_config, yaml
|
||||
from ovgenpy.config import register_config, yaml, Alias
|
||||
|
||||
|
||||
# YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html
|
||||
|
||||
# Load/dump infrastructure testing
|
||||
from ovgenpy.utils.keyword_dataclasses import fields
|
||||
|
||||
|
||||
def test_register_config():
|
||||
@register_config
|
||||
|
@ -33,6 +36,8 @@ def test_yaml_object():
|
|||
assert s == '!Bar {}\n'
|
||||
|
||||
|
||||
# Dataclass dump testing
|
||||
|
||||
def test_dump_defaults():
|
||||
@register_config
|
||||
class Config:
|
||||
|
@ -71,7 +76,46 @@ b: b
|
|||
'''
|
||||
|
||||
|
||||
def test_argument_validation():
|
||||
# Dataclass load testing
|
||||
|
||||
|
||||
def test_dump_load_aliases():
|
||||
""" Ensure dumping and loading `xx=Alias('x')` works.
|
||||
Ensure loading `{x=1, xx=1}` raises an error.
|
||||
Does not check constructor `Config(xx=1)`. """
|
||||
@register_config
|
||||
class Config:
|
||||
x: int
|
||||
xx = Alias('x')
|
||||
|
||||
# Test dumping
|
||||
assert len(fields(Config)) == 1
|
||||
cfg = Config(1)
|
||||
s = yaml.dump(cfg)
|
||||
assert s == '''\
|
||||
!Config
|
||||
x: 1
|
||||
'''
|
||||
assert yaml.load(s) == cfg
|
||||
|
||||
# Test loading
|
||||
s = '''\
|
||||
!Config
|
||||
xx: 1
|
||||
'''
|
||||
assert yaml.load(s) == Config(x=1)
|
||||
|
||||
# Test exception on duplicated parameters.
|
||||
s = '''\
|
||||
!Config
|
||||
x: 1
|
||||
xx: 1
|
||||
'''
|
||||
with pytest.raises(TypeError):
|
||||
yaml.load(s)
|
||||
|
||||
|
||||
def test_load_argument_validation():
|
||||
""" Ensure that loading config via YAML catches missing and invalid parameters. """
|
||||
@register_config
|
||||
class Config:
|
||||
|
@ -93,7 +137,6 @@ def test_argument_validation():
|
|||
''')
|
||||
|
||||
|
||||
|
||||
def test_load_post_init():
|
||||
""" yaml.load() does not natively call __post_init__. So @register_config modifies
|
||||
__setstate__ to call __post_init__. """
|
||||
|
|
Ładowanie…
Reference in New Issue