Skip to content

Commit 0f54246

Browse files
author
Lariel Fernandes
committed
feat: built-in to_yaml filter for jinja yaml config loader
1 parent 3c5ec3d commit 0f54246

3 files changed

Lines changed: 1551 additions & 1493 deletions

File tree

src/mlopus/utils/jinja_yaml.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import textwrap
23
from collections import defaultdict
34
from pathlib import Path
45
from typing import Any, Literal, Iterator, Tuple, Callable
@@ -47,6 +48,11 @@ def load_jinja_yaml_configs(
4748
:param file_extensions: File extensions to use. Defaults to `{'.yml', '.yaml'}`. The leading dot is ignored.
4849
"""
4950

51+
custom_filters = custom_filters or {}
52+
for func in _FILTERS:
53+
if func.__name__ not in custom_filters:
54+
custom_filters[func.__name__] = func
55+
5056
namespace_files: dict[str, list[Path]] = defaultdict(list)
5157
for namespace, file_path in _iter_files_with_namespaces(Path(base_path), file_extensions or {".yml", ".yaml"}):
5258
namespace_files[namespace].append(file_path)
@@ -94,7 +100,7 @@ def load_jinja_yaml_configs(
94100
return result
95101

96102

97-
def _iter_files_with_namespaces(base_path: Path, extensions: list[str]) -> Iterator[Tuple[str, Path]]:
103+
def _iter_files_with_namespaces(base_path: Path, extensions: set[str]) -> Iterator[Tuple[str, Path]]:
98104
"""Iterate (namespace, path) tuples for every YAML file in the specified path.
99105
100106
Namespaces are determined by the file's top dir or by its name before any double underscores.
@@ -108,3 +114,35 @@ def _iter_files_with_namespaces(base_path: Path, extensions: list[str]) -> Itera
108114
rel_path = file_path.relative_to(base_path)
109115
namespace = rel_path.parts[0] if len(rel_path.parts) > 1 else file_path.stem.split("__")[0]
110116
yield namespace, file_path
117+
118+
119+
class _NotSet:
120+
pass
121+
122+
123+
class _Filters:
124+
"""Built-in jinja filters."""
125+
126+
@staticmethod
127+
def to_yaml(
128+
arg: Any,
129+
*,
130+
indent: int = 0,
131+
if_none: Any | None = _NotSet,
132+
if_falsy: Any | None = _NotSet,
133+
) -> str:
134+
if arg is None and if_none is not _NotSet:
135+
arg = if_none
136+
137+
if not arg and if_falsy is not _NotSet:
138+
arg = if_falsy
139+
140+
encoded = yaml.safe_dump(arg).removesuffix("\n").removesuffix("\n...")
141+
142+
if prefix := indent * " ":
143+
encoded = textwrap.indent(encoded, prefix).removeprefix(prefix)
144+
145+
return encoded
146+
147+
148+
_FILTERS = (_Filters.to_yaml,)

src/tests/test_utils/test_jinja_yaml.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ def test_load_jinja_yaml_configs_basic():
2121
bar: foo
2222
with_suffix: {{ "foo" | add_suffix }}
2323
empty_key: ""
24+
this_is_none: null
25+
this_is_falsy_str: ""
26+
this_is_falsy_int: 0
27+
this_is_falsy_float: 0.0
28+
dict_with_nones:
29+
key1: null
30+
key2: [null]
2431
""")
2532
)
2633

@@ -40,6 +47,12 @@ def test_load_jinja_yaml_configs_basic():
4047
{% for key, value in common.conn_defaults.items() %}
4148
{{ key }}: {{ value }}
4249
{% endfor %}
50+
this_is_none: {{ common.this_is_none | to_yaml(if_none="") }}
51+
this_is_falsy_str: {{ common.this_is_falsy_str | to_yaml(if_falsy=None) }}
52+
this_is_falsy_int: {{ common.this_is_falsy_int | to_yaml(if_falsy=None) }}
53+
this_is_falsy_float: {{ common.this_is_falsy_float | to_yaml(if_falsy=None) }}
54+
dict_with_nones:
55+
{{ common.dict_with_nones | to_yaml(indent=2) }}
4356
""")
4457
)
4558

@@ -71,3 +84,10 @@ def test_load_jinja_yaml_configs_basic():
7184
assert result["database"]["missing_gets_fallback"] == "fallback"
7285
assert result["database"]["empty_gets_fallback"] == "fallback"
7386
assert result["database"]["conn_defaults"] == result["common"]["conn_defaults"]
87+
88+
assert result["database"]["this_is_none"] == ""
89+
assert result["database"]["this_is_falsy_str"] is None
90+
assert result["database"]["this_is_falsy_int"] is None
91+
assert result["database"]["this_is_falsy_float"] is None
92+
93+
assert result["database"]["dict_with_nones"] == {"key1": None, "key2": [None]}

0 commit comments

Comments
 (0)