Skip to content

Commit 80b3339

Browse files
committed
feat: initial checkin
1 parent 985ea54 commit 80b3339

File tree

7 files changed

+742
-4
lines changed

7 files changed

+742
-4
lines changed

src/annotatedyaml/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1-
__version__ = "0.0.0"
1+
"""YAML utility functions."""
2+
3+
from .const import SECRET_YAML
4+
from .dumper import dump, save_yaml
5+
from .input import UndefinedSubstitution, extract_inputs, substitute
6+
from .loader import (
7+
Secrets,
8+
YamlTypeError,
9+
load_yaml,
10+
load_yaml_dict,
11+
parse_yaml,
12+
secret_yaml,
13+
)
14+
from .objects import Input
15+
16+
__all__ = [
17+
"SECRET_YAML",
18+
"Input",
19+
"Secrets",
20+
"UndefinedSubstitution",
21+
"YamlTypeError",
22+
"dump",
23+
"extract_inputs",
24+
"load_yaml",
25+
"load_yaml_dict",
26+
"parse_yaml",
27+
"save_yaml",
28+
"secret_yaml",
29+
"substitute",
30+
]

src/annotatedyaml/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Constants."""
2+
3+
SECRET_YAML = "secrets.yaml"

src/annotatedyaml/dumper.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Custom dumper and representers."""
2+
3+
from collections import OrderedDict
4+
from typing import Any
5+
6+
import yaml
7+
8+
from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass
9+
10+
# mypy: allow-untyped-calls, no-warn-return-any
11+
12+
13+
try:
14+
from yaml import CSafeDumper as FastestAvailableSafeDumper
15+
except ImportError:
16+
from yaml import ( # type: ignore[assignment]
17+
SafeDumper as FastestAvailableSafeDumper,
18+
)
19+
20+
21+
def dump(_dict: dict | list) -> str:
22+
"""Dump YAML to a string and remove null."""
23+
return yaml.dump(
24+
_dict,
25+
default_flow_style=False,
26+
allow_unicode=True,
27+
sort_keys=False,
28+
Dumper=FastestAvailableSafeDumper,
29+
).replace(": null\n", ":\n")
30+
31+
32+
def save_yaml(path: str, data: dict) -> None:
33+
"""Save YAML to a file."""
34+
# Dump before writing to not truncate the file if dumping fails
35+
str_data = dump(data)
36+
with open(path, "w", encoding="utf-8") as outfile:
37+
outfile.write(str_data)
38+
39+
40+
# From: https://gist.github.com/miracle2k/3184458
41+
def represent_odict( # type: ignore[no-untyped-def]
42+
dumper, tag, mapping, flow_style=None
43+
) -> yaml.MappingNode:
44+
"""Like BaseRepresenter.represent_mapping but does not issue the sort()."""
45+
value: list = []
46+
node = yaml.MappingNode(tag, value, flow_style=flow_style)
47+
if dumper.alias_key is not None:
48+
dumper.represented_objects[dumper.alias_key] = node
49+
best_style = True
50+
if hasattr(mapping, "items"):
51+
mapping = mapping.items()
52+
for item_key, item_value in mapping:
53+
node_key = dumper.represent_data(item_key)
54+
node_value = dumper.represent_data(item_value)
55+
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
56+
best_style = False
57+
if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style):
58+
best_style = False
59+
value.append((node_key, node_value))
60+
if flow_style is None:
61+
if dumper.default_flow_style is not None:
62+
node.flow_style = dumper.default_flow_style
63+
else:
64+
node.flow_style = best_style
65+
return node
66+
67+
68+
def add_representer(klass: Any, representer: Any) -> None:
69+
"""Add to representer to the dumper."""
70+
FastestAvailableSafeDumper.add_representer(klass, representer)
71+
72+
73+
add_representer(
74+
OrderedDict,
75+
lambda dumper, value: represent_odict(dumper, "tag:yaml.org,2002:map", value),
76+
)
77+
78+
add_representer(
79+
NodeDictClass,
80+
lambda dumper, value: represent_odict(dumper, "tag:yaml.org,2002:map", value),
81+
)
82+
83+
add_representer(
84+
NodeListClass,
85+
lambda dumper, value: dumper.represent_sequence("tag:yaml.org,2002:seq", value),
86+
)
87+
88+
add_representer(
89+
NodeStrClass,
90+
lambda dumper, value: dumper.represent_scalar("tag:yaml.org,2002:str", str(value)),
91+
)
92+
93+
add_representer(
94+
Input,
95+
lambda dumper, value: dumper.represent_scalar("!input", value.name),
96+
)

src/annotatedyaml/input.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Deal with YAML input."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from .objects import Input
8+
9+
10+
class UndefinedSubstitution(Exception):
11+
"""Error raised when we find a substitution that is not defined."""
12+
13+
def __init__(self, input_name: str) -> None:
14+
"""Initialize the undefined substitution exception."""
15+
super().__init__(f"No substitution found for input {input_name}")
16+
self.input = input
17+
18+
19+
def extract_inputs(obj: Any) -> set[str]:
20+
"""Extract input from a structure."""
21+
found: set[str] = set()
22+
_extract_inputs(obj, found)
23+
return found
24+
25+
26+
def _extract_inputs(obj: Any, found: set[str]) -> None:
27+
"""Extract input from a structure."""
28+
if isinstance(obj, Input):
29+
found.add(obj.name)
30+
return
31+
32+
if isinstance(obj, list):
33+
for val in obj:
34+
_extract_inputs(val, found)
35+
return
36+
37+
if isinstance(obj, dict):
38+
for val in obj.values():
39+
_extract_inputs(val, found)
40+
return
41+
42+
43+
def substitute(obj: Any, substitutions: dict[str, Any]) -> Any:
44+
"""Substitute values."""
45+
if isinstance(obj, Input):
46+
if obj.name not in substitutions:
47+
raise UndefinedSubstitution(obj.name)
48+
return substitutions[obj.name]
49+
50+
if isinstance(obj, list):
51+
return [substitute(val, substitutions) for val in obj]
52+
53+
if isinstance(obj, dict):
54+
return {key: substitute(val, substitutions) for key, val in obj.items()}
55+
56+
return obj

0 commit comments

Comments
 (0)