From 7f34c5868ab2a9d1588b1b1415796e6807125850 Mon Sep 17 00:00:00 2001 From: Sylvain Laperche Date: Tue, 26 Nov 2019 08:59:45 +0100 Subject: [PATCH] buildchain/targets: add a YAML Renderer Add a new serialization format: YAML. This can be used as base for an SLS serializer. In order to handle long string payload and binary content we add two helpers (`YAMLDocument.text` and `YAMLDocument.bytestring`) that use custom renderers in order to have a readable encoding (using `|` string block literal) and right encoding (use Base64 for binary contents). Refs: #2070 Signed-off-by: Sylvain Laperche --- buildchain/buildchain/targets/__init__.py | 4 +- buildchain/buildchain/targets/serialize.py | 60 +++++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/buildchain/buildchain/targets/__init__.py b/buildchain/buildchain/targets/__init__.py index 1b98a02472..5b89475c1d 100644 --- a/buildchain/buildchain/targets/__init__.py +++ b/buildchain/buildchain/targets/__init__.py @@ -15,7 +15,7 @@ from buildchain.targets.repository import ( Repository, RPMRepository, DEBRepository ) -from buildchain.targets.serialize import Renderer, SerializedData +from buildchain.targets.serialize import Renderer, SerializedData, YAMLDocument from buildchain.targets.template import TemplateFile # For mypy, see `--no-implicit-reexport` documentation. @@ -29,6 +29,6 @@ 'Package', 'RPMPackage', 'DEBPackage', 'RemoteImage', 'Repository', 'RPMRepository', 'DEBRepository', - 'Renderer', 'SerializedData', + 'Renderer', 'SerializedData', 'YAMLDocument', 'TemplateFile', ] diff --git a/buildchain/buildchain/targets/serialize.py b/buildchain/buildchain/targets/serialize.py index e45bbba0a7..dec685a530 100644 --- a/buildchain/buildchain/targets/serialize.py +++ b/buildchain/buildchain/targets/serialize.py @@ -2,10 +2,13 @@ """Targets to write files from Python objects.""" +import base64 import enum import json from pathlib import Path -from typing import Any, Callable, Mapping +from typing import Any, Callable, Dict, Mapping + +import yaml from buildchain import types from buildchain import utils @@ -29,18 +32,34 @@ def render_envfile(variables: Mapping[str, str], filepath: Path) -> None: fp.write('\n') +def render_yaml(data: Any, filepath: Path) -> None: + """Serialize an object as YAML to a given file path.""" + with filepath.open('w', encoding='utf-8') as fp: + dumper = yaml.SafeDumper(fp, sort_keys=False) + dumper.add_representer(YAMLDocument.Literal, _literal_representer) + dumper.add_representer(YAMLDocument.ByteString, _bytestring_representer) + try: + dumper.open() + dumper.represent(data) + dumper.close() + finally: + dumper.dispose() + + class Renderer(enum.Enum): """Supported rendering methods for `SerializedData` targets.""" JSON = 'JSON' ENV = 'ENV' + YAML = 'YAML' class SerializedData(base.AtomicTarget): """Serialize an object into a file with a specific renderer.""" - RENDERERS = { + RENDERERS : Dict[Renderer, Callable[[Any, Path], None]] = { Renderer.JSON: render_json, Renderer.ENV: render_envfile, + Renderer.YAML: render_yaml, } def __init__( @@ -95,3 +114,40 @@ def _render(self) -> Callable[[Any, Path], None]: def _run(self) -> None: """Render the file.""" self._render(self._data, self._dest) + + +# YAML {{{ + + +class YAMLDocument(): + """A YAML document, with an optional preamble (like a shebang).""" + class Literal(str): + """A large block of text, to be rendered as a block scalar.""" + + class ByteString(bytes): + """A binary string, to be rendered as a base64-encoded literal.""" + + @classmethod + def text(cls, value: str) -> 'YAMLDocument.Literal': + """Cast the value to a Literal.""" + return cls.Literal(value) + + @classmethod + def bytestring(cls, value: bytes) -> 'YAMLDocument.ByteString': + """Cast the value to a ByteString.""" + return cls.ByteString(value) + + +def _literal_representer(dumper: yaml.BaseDumper, data: Any) -> Any: + scalar = yaml.representer.SafeRepresenter.represent_str(dumper, data) + scalar.style = '|' + return scalar + + +def _bytestring_representer(dumper: yaml.BaseDumper, data: Any) -> Any: + return _literal_representer( + dumper, base64.encodebytes(data).decode('utf-8') + ) + + +# }}}