Add Alias class, for config field aliasing

pull/357/head
nyanpasu64 2018-08-25 22:16:21 -07:00
rodzic acffc1a55e
commit 56438bed5c
2 zmienionych plików z 93 dodań i 16 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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__. """