madeleine/madeleine/base.py

234 lines
6.7 KiB
Python
Raw Normal View History

2019-09-16 08:53:38 +00:00
from abc import ABCMeta, abstractmethod, abstractproperty
from collections.abc import Mapping, Iterable
from enum import Enum
from pathlib import Path
from objtools.registry import ClassRegistry
import random
class FileFormat(Enum):
JSON = 'json'
YAML = 'yaml'
2019-09-22 17:54:46 +00:00
TOML = 'toml'
2019-09-16 08:53:38 +00:00
class ComponentRegistry(ClassRegistry):
def check_value(self, value):
assert issubclass(value, Component)
registry = ComponentRegistry()
register = registry.register
unregister = registry.unregister
class ComponentMetaclass(registry.metaclass, ABCMeta):
pass
class Component(metaclass=ComponentMetaclass, register=False):
"""
Describes any generator component.
"""
def __init__(self, **data):
self.__dict__.update(**data)
def resolve_references(self, references):
"""
Complex components might hold references to other components:
they should leave them unresolved until the Generator builds all the
components, then calls this method with all of the referencable names.
"""
@abstractmethod
def generate(self):
pass
2019-09-28 13:35:29 +00:00
def serialize(self):
"""
Should return a JSON/YAML/TOML-compatible structure which can be used
to export a component to a file.
"""
return self.__dict__.copy()
2019-09-16 08:53:38 +00:00
@abstractproperty
def combinations(self):
"""
Should return how many combinations a single component may have.
Useful to perform some checks after parsing.
"""
def __str__(self):
return str(self.serialize())
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__,
', '.join(
'{}={!r}'.format(k, v)
for k, v in self.serialize().items()
),
)
def _repr_pretty_(self, p, cycle):
if cycle:
p.text('{}(...)'.format(self.__class__.__name__))
else:
with p.group(2, '{}('.format(self.__class__.__name__), ')'):
p.breakable('')
for k, v in self.serialize().items():
p.text(k)
p.text('=')
p.pretty(v)
p.text(',')
p.breakable()
2019-09-16 08:53:38 +00:00
@classmethod
def from_path(cls, path, fmt=FileFormat.YAML):
assert isinstance(fmt, FileFormat)
if not isinstance(path, Path):
path = Path(path)
with path.open() as f:
2019-09-22 17:54:46 +00:00
if fmt == FileFormat.JSON:
import json
data = json.load(f)
elif fmt == FileFormat.YAML:
try:
import yaml
except ImportError as e:
raise ImportError(
'{}\n'
'You may need to install madeleine with YAML support: '
2019-09-22 17:54:46 +00:00
'pip install madeleine[yaml]'.format(e.message),
)
data = yaml.safe_load(f)
elif fmt == FileFormat.TOML:
try:
import toml
except ImportError as e:
raise ImportError(
'{}\n'
'You may need to install madeleine with TOML support: '
2019-09-22 17:54:46 +00:00
'pip install madeleine[toml]'.format(e.message),
)
data = toml.load(f)
else:
raise NotImplementedError
2019-09-16 08:53:38 +00:00
if cls is Component:
# Guess the component type when calling Component.from_path
2019-09-22 17:54:46 +00:00
return Component._make(data)
return cls(**data)
2019-09-16 08:53:38 +00:00
@staticmethod
def _make(data):
"""
Guess which component subclass to use for a given component data,
then build and return the resulting components.
"""
if isinstance(data, str):
data = {'value': data}
if not isinstance(data, Mapping):
if isinstance(data, Iterable):
return list(map(Component._make, data))
raise ValueError('Component description should be a mapping')
for key, component_class in registry.items():
if key in data:
return component_class(**data)
raise ValueError('Could not parse component description')
class Reference(Component, key='ref'):
"""
A component used to temporarily hold references to other components
during the first pass of schema loading, when not all referenced names
are available, before a second pass allows resolution of all references.
"""
def __init__(self, **kwargs):
self.to = None
super().__init__(**kwargs)
def resolve_references(self, references):
if self.ref not in references:
raise ValueError('Unknown reference {!r}'.format(self.ref))
self.to = references[self.ref]
2019-09-16 08:53:38 +00:00
@property
def combinations(self):
if not self.to:
raise TypeError('Unresolved reference to {!r}'.format(self.ref))
return self.to.combinations
2019-09-16 08:53:38 +00:00
def generate(self):
if not self.to:
raise TypeError('Unresolved reference to {!r}'.format(self.ref))
return self.to.generate()
2019-09-16 08:53:38 +00:00
2019-09-28 13:35:29 +00:00
def serialize(self):
return {
'ref': self.ref,
}
2019-09-16 08:53:38 +00:00
class Value(Component, key='value'):
"""
A component that holds a string value.
Will return the value on every generation, unless `optional` is True,
in which case it may return None 50% of the time.
"""
def __init__(self, value, optional=False, **kwargs):
self.value = value
self.optional = optional
super().__init__(**kwargs)
@property
def combinations(self):
return 1 + self.optional
def generate(self):
if not self.optional or random.randrange(2):
return self.value
2019-09-28 13:35:29 +00:00
def serialize(self):
if self.optional:
return {
'optional': True,
'value': self.value,
}
return self.value
2019-09-16 08:53:38 +00:00
class Include(Component, key='include'):
2019-09-16 08:53:38 +00:00
"""
Automatically include another generator file.
"""
def __init__(self, include=None, format=None, **kwargs):
2019-09-16 08:53:38 +00:00
if format is None:
format = FileFormat.YAML
if isinstance(format, str):
format = FileFormat(format)
super().__init__(include=include, format=format, **kwargs)
self.to = Component.from_path(self.include, fmt=self.format)
def resolve_references(self, references):
self.to.resolve_references(references)
@property
def combinations(self):
return self.to.combinations
def generate(self):
return self.to.generate()
2019-09-28 13:35:29 +00:00
def serialize(self):
return {
'include': self.include,
'format': self.format,
}