-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
80 additions
and
178 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,146 +1,53 @@ | ||
import yaml | ||
import textwrap | ||
import itertools | ||
from glorpen.config.translators.base import Renderer, Reader | ||
from io import StringIO | ||
import typing | ||
|
||
# TODO: comment all but first variants, key alternatives | ||
# TODO: comment char at line start? | ||
import yaml | ||
|
||
class YamlRenderer(Renderer): | ||
def reset(self): | ||
self._data = StringIO() | ||
self._key = [] | ||
self._value_prefix = [] | ||
self._first_list_item = False | ||
self._parent_containers = [] | ||
from glorpen.config.model.schema import Field | ||
|
||
def finish(self): | ||
ret = self._data.getvalue() | ||
self._data.close() | ||
return ret | ||
|
||
def padding(self, diff = 0): | ||
return " " * ((len(self._key) + diff) * 2) | ||
|
||
def render_value(self, value): | ||
ret_v = yaml.dump(value, explicit_start=False, explicit_end=False).strip() | ||
if ret_v.endswith("\n..."): | ||
ret_v = ret_v[:-4].strip() | ||
return ret_v | ||
|
||
def render_key(self, k): | ||
if k is None: | ||
return '- ' | ||
else: | ||
return f'{k}: ' | ||
def list_indent(items: typing.Iterable[str], prefix): | ||
for item in items: | ||
yield f"{prefix}{item}" | ||
|
||
def render_current_parent_key(self): | ||
return self.render_key(self._key[-1]) | ||
|
||
def get_current_keys_until_main_list_start(self): | ||
ret = list(reversed(list(itertools.takewhile(lambda x: x is None, reversed(self._key[0:-1]))))) | ||
if ret: | ||
ret.append(self._key[-1]) | ||
else: | ||
if len(self._key) > 0 and self._key[-1] is None: | ||
ret.append(None) | ||
|
||
return ret | ||
|
||
def get_current_container(self): | ||
return self._parent_containers[-1] if self._parent_containers else None | ||
class YamlRenderer: | ||
|
||
def render_value_as_variant(self, value, comment=''): | ||
z = -1 | ||
# when in nested list item, parent key list is on same line | ||
if self._first_list_item: | ||
# it could be nested list or hash in a list | ||
# so handle laying multiple keys in one line | ||
keys = self.get_current_keys_until_main_list_start() | ||
keys_prefix = "".join(self.render_key(i) for i in keys) | ||
z = z - len(keys) + 1 | ||
else: | ||
keys_prefix = self.render_current_parent_key() | ||
|
||
return self.padding(z) + keys_prefix + self.render_value(value) + comment + '\n' | ||
def __init__(self): | ||
super(YamlRenderer, self).__init__() | ||
|
||
def render(self, model: Field): | ||
return "\n".join(list(self._render(model))) + "\n" | ||
|
||
def visit_node(self, node): | ||
if hasattr(node, "description"): | ||
self._data.write(self.padding(-1) + f"# {node.description}\n") | ||
|
||
def visit_variant(self, variant, node): | ||
if variant.has_description: | ||
self._data.write(self.padding(-1) + f"# {variant.description}" + "\n") | ||
|
||
if variant.has_value: | ||
value = variant.value | ||
comment = '' | ||
def _render(self, model: Field): | ||
if isinstance(model.args, dict): | ||
yield from self._render_dict(model.args) | ||
else: | ||
value = 'something' | ||
comment = ' # required and no default' | ||
|
||
self._data.write(self.render_value_as_variant(value, comment)) | ||
|
||
def leave_variant(self, variant, node): | ||
self._first_list_item = False | ||
|
||
def visit_node_variants(self, node): | ||
if not node.variants: | ||
parent = self.get_current_container() | ||
if parent and parent.has_value: | ||
self._data.write(self.render_value_as_variant(parent.value)) | ||
yield from self._render_value(model) | ||
|
||
def _render_dict(self, fields: typing.Dict[str, Field]): | ||
for name, field in fields.items(): | ||
if field.doc: | ||
yield from list_indent(textwrap.wrap(field.doc, width=60), "# ") | ||
value = list(self._render(field)) | ||
key = f"{name}: " | ||
prefix = "# " if field.default_factory else "" | ||
yield prefix + key + value[0] | ||
for line in list_indent(value[1:], " " * len(key)): | ||
yield prefix + line | ||
|
||
def _render_value(self, model: Field): | ||
if model.is_nullable(): | ||
yield "~" | ||
elif model.default_factory: | ||
msg = yaml.safe_dump(model.default_factory(), default_style='|') | ||
if msg.endswith("\n...\n"): | ||
msg = msg[:-5] | ||
lines = msg.splitlines(keepends=False) | ||
if lines[0] == "|-" and len(lines) == 2: | ||
yield lines[1].lstrip() | ||
else: | ||
self._data.write(self.render_value_as_variant('something', ' # required and no default')) | ||
|
||
def leave_node_variants(self, node): | ||
# when no variants exist in this node | ||
self._first_list_item = False | ||
|
||
def visit_item_hash(self, k, node): | ||
self._key.append(k) | ||
|
||
def leave_item_hash(self, k, node): | ||
self._key.pop() | ||
|
||
def visit_item_list(self, node): | ||
self._key.append(None) | ||
|
||
def leave_item_list(self, node): | ||
self._key.pop() | ||
self._first_list_item = False | ||
|
||
def visit_container(self, node): | ||
self._parent_containers.append(node) | ||
|
||
def leave_container(self, node): | ||
self._parent_containers.pop() | ||
|
||
def visit_container_list(self, node): | ||
self._first_list_item = True | ||
if self._key and self._key[-1] is not None: | ||
self._data.write(self.padding(-1) + self.render_current_parent_key() + "\n") | ||
|
||
def visit_container_hash(self, node): | ||
if self._key and self._key[-1] is not None: | ||
self._data.write(self.padding(-1) + self.render_current_parent_key() + "\n") | ||
|
||
def visit_item_alternative(self, node): | ||
# if alternatives parent is a list, treat each child as new list entry | ||
if self._parent_containers[-2].are_children_list: | ||
self._first_list_item = True | ||
|
||
# def visit_any(self, tag, node, *args): | ||
# self._data.write(f"<{tag}>\n") | ||
# def leave_any(self, tag, node, *args): | ||
# self._data.write(f"</{tag}>\n") | ||
|
||
class YamlReader(Reader): | ||
def __init__(self, path): | ||
super().__init__() | ||
self.path = path | ||
|
||
def read(self): | ||
with open(self.path, "rt") as f: | ||
return yaml.safe_load(f.read()) | ||
yield lines[0] | ||
yield from textwrap.dedent("\n".join(lines[1:])).splitlines(keepends=False) | ||
else: | ||
yield f"# required {model.type.__name__}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,46 @@ | ||
import unittest | ||
import yaml | ||
import dataclasses | ||
import typing | ||
|
||
from glorpen.config.fields.simple import Dict, List, Any, Variant | ||
from glorpen.config import Schema | ||
from glorpen.config.translators.yaml import YamlRenderer | ||
|
||
class YamlRendererTest(unittest.TestCase): | ||
|
||
def _render_and_load(self, f): | ||
help_str = YamlRenderer().render(f.help_config) | ||
return yaml.safe_load(help_str) | ||
|
||
def test_dict_nested_in_list(self): | ||
f = Dict({ | ||
"a-dict": List( | ||
Dict({'a':Any().help(value=1), 'i':Any().help(value=1)}), | ||
) | ||
}) | ||
|
||
out = self._render_and_load(f) | ||
self.assertEqual(out, {'a-dict':[{'a': 1, 'i': 1}]}) | ||
|
||
def test_dict_with_first_list_item(self): | ||
f = List(Any().help(value="test")) | ||
out = self._render_and_load(f) | ||
self.assertEqual(out, ['test']) | ||
|
||
def test_alternative_nested_lists(self): | ||
f = List( | ||
Variant([ | ||
Dict({'a':Any().help(value=1), 'i':Any().help(value=1)}), | ||
Dict({'b':Any().help(value=1)}), | ||
]) | ||
|
||
def test_asd(): | ||
@dataclasses.dataclass | ||
class Dummy: | ||
comment_field: str = dataclasses.field(metadata={"doc": "some string value"}) | ||
required_field: str | ||
nullable_field: typing.Optional[str] | ||
optional_field: str = dataclasses.field(default="test1") | ||
optional_comment_field: str = dataclasses.field( | ||
default="test2", metadata={"doc": """some multiline | ||
string value""" | ||
} | ||
) | ||
|
||
out = self._render_and_load(f) | ||
self.assertEqual(out, [{'a':1, 'i':1}, {'b':1}]) | ||
r = YamlRenderer() | ||
|
||
def test_nested_dict_inside_list(self): | ||
f = List( | ||
Dict({ | ||
"k1": Dict({ | ||
"k2": Any().help(value=True) | ||
}) | ||
}) | ||
) | ||
model = Schema().generate(Dummy) | ||
assert r.render(model) == """# some string value | ||
comment_field: # required str | ||
required_field: # required str | ||
nullable_field: ~ | ||
# optional_field: test1 | ||
# some multiline | ||
# string value | ||
# optional_comment_field: test2 | ||
""" | ||
|
||
|
||
def test_multiline_defaults(): | ||
@dataclasses.dataclass | ||
class Dummy: | ||
field: str = """line1 | ||
line2 | ||
line3""" | ||
|
||
model = Schema().generate(Dummy) | ||
r = YamlRenderer() | ||
data = r.render(model) | ||
|
||
help_str = YamlRenderer().render(f.help_config) | ||
# out = self._render_and_load(f) | ||
print(help_str) | ||
# self.assertEqual(out, [{'a':1, 'i':1}, {'b':1}]) | ||
assert data == "# field: |-\n# line1\n# line2\n# line3\n" |